├── .github ├── actions │ ├── build │ │ └── action.yml │ └── test │ │ └── action.yml └── workflows │ ├── production.yml │ ├── pull-request-close.yml │ ├── pull-request.yml │ └── push-master.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── backend ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierrc ├── Dockerfile ├── __tests__ │ ├── config.spec.ts │ ├── custom-provider │ │ ├── aave-custom-provider.spec.ts │ │ └── aave-provider-manager.spec.ts │ ├── graphql │ │ └── validators │ │ │ └── index.spec.ts │ ├── helpers │ │ ├── ethereum.spec.ts │ │ └── utils.spec.ts │ ├── mocks.ts │ ├── services │ │ ├── incentives-data │ │ │ ├── provider.spec.ts │ │ │ └── rpc.spec.ts │ │ ├── pool-data │ │ │ ├── provider.spec.ts │ │ │ ├── provier.spec.ts │ │ │ └── rpc.spec.ts │ │ └── stake-data │ │ │ ├── provier.spec.ts │ │ │ └── rpc.spec.ts │ ├── setup-jest.ts │ └── tasks │ │ ├── last-seen-block.state.spec.ts │ │ ├── task-helpers.spec.ts │ │ ├── update-block-number │ │ └── handler.spec.ts │ │ ├── update-general-reserves-data │ │ └── handler.spec.ts │ │ ├── update-reserve-incentives-data │ │ └── handler.spec.ts │ │ ├── update-stake-general-ui-data │ │ ├── handler.spec.ts │ │ └── last-stored-hash.state.spec.ts │ │ ├── update-stake-user-ui-data │ │ └── handler.spec.ts │ │ ├── update-users-data │ │ ├── handler.spec.ts │ │ ├── pool-contracts.state.spec.ts │ │ └── protocol-data-reserves.state.spec.ts │ │ └── update-users-incentives-data │ │ ├── handle.spec.ts │ │ ├── pool-contracts.state.spec.ts │ │ └── pool-incentives-data.state.spec.ts ├── babel.config.js ├── jest.config.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── src │ ├── api.ts │ ├── config.ts │ ├── contracts │ │ └── ethers │ │ │ ├── ILendingPool.d.ts │ │ │ ├── ILendingPoolAddressesProvider.d.ts │ │ │ ├── ILendingPoolAddressesProviderFactory.ts │ │ │ ├── ILendingPoolFactory.ts │ │ │ ├── StakeUiHelperFactory.ts │ │ │ └── StakeUiHelperI.d.ts │ ├── custom-provider │ │ ├── aave-custom-provider.ts │ │ ├── aave-provider-manager.ts │ │ └── models │ │ │ ├── aave-provider-context.ts │ │ │ ├── already-used-node-context.ts │ │ │ ├── base-provider-context.ts │ │ │ ├── internal-aave-provider-context.ts │ │ │ └── main-node-reconnection-settings.ts │ ├── env-helpers.ts │ ├── graphql │ │ ├── object-types │ │ │ ├── incentives.ts │ │ │ ├── reserve.ts │ │ │ ├── stake-general-data.ts │ │ │ ├── stake-user-data.ts │ │ │ ├── user-incentives.ts │ │ │ └── user-reserve.ts │ │ ├── resolvers │ │ │ ├── health-resolver.ts │ │ │ ├── incentives-data-resolver.ts │ │ │ ├── index.ts │ │ │ ├── protocol-data-resolver.ts │ │ │ └── stake-data-resolver.ts │ │ └── validators │ │ │ └── index.ts │ ├── helpers │ │ ├── ethereum.ts │ │ └── utils.ts │ ├── pubsub.ts │ ├── redis │ │ ├── block-number.ts │ │ ├── index.ts │ │ ├── pool-incentives.ts │ │ ├── pool-user-incentives.ts │ │ ├── protocol-data.ts │ │ ├── protocol-user-data.ts │ │ ├── shared.ts │ │ ├── stake-general-ui-data.ts │ │ └── stake-user-ui-data.ts │ ├── services │ │ ├── incentives-data │ │ │ ├── index.ts │ │ │ ├── provider.ts │ │ │ └── rpc.ts │ │ ├── pool-data │ │ │ ├── index.ts │ │ │ ├── provider.ts │ │ │ └── rpc.ts │ │ └── stake-data │ │ │ ├── index.ts │ │ │ ├── provider.ts │ │ │ └── rpc.ts │ └── tasks │ │ ├── last-seen-block.state.ts │ │ ├── task-helpers.ts │ │ ├── update-block-number │ │ ├── handler.ts │ │ └── run.ts │ │ ├── update-general-reserves-data │ │ ├── handler.ts │ │ └── run.ts │ │ ├── update-reserve-incentives-data │ │ ├── handler.ts │ │ └── run.ts │ │ ├── update-stake-general-ui-data │ │ ├── handler.ts │ │ ├── last-stored-hash.state.ts │ │ └── run.ts │ │ ├── update-stake-user-ui-data │ │ ├── handler.ts │ │ └── run.ts │ │ ├── update-users-data │ │ ├── handler.ts │ │ ├── pool-contracts.state.ts │ │ ├── protocol-data-reserves.state.ts │ │ └── run.ts │ │ └── update-users-incentives-data │ │ ├── handler.ts │ │ ├── pool-contracts.state.ts │ │ ├── pool-incentives-data.state.ts │ │ └── run.ts └── tsconfig.json ├── docker-compose.yml └── k8s ├── backend_components.py ├── common.py ├── ingress.py ├── main.py ├── redis_component.py ├── render.sh └── values.py /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build image 2 | description: Build docker image 3 | inputs: 4 | REGISTRY: 5 | description: registry to push to 6 | required: false 7 | default: ghcr.io 8 | outputs: 9 | image_name: 10 | description: Full image name 11 | value: ${{ steps.set_image_name.outputs.image_name }} 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v1 17 | 18 | - name: Login to GHCR 19 | uses: docker/login-action@v1 20 | with: 21 | registry: ${{ inputs.REGISTRY }} 22 | username: ${{ github.repository_owner }} 23 | password: ${{ github.token }} 24 | 25 | - name: Set full image name 26 | id: set_image_name 27 | shell: bash 28 | run: | 29 | echo "::set-output name=image_name::${{ inputs.REGISTRY }}/${REPO_NAME,,}:${{ github.sha }}" 30 | env: 31 | REPO_NAME: '${{ github.repository }}' 32 | 33 | - name: Build and push 34 | uses: docker/build-push-action@v2 35 | with: 36 | context: backend 37 | platforms: linux/amd64 38 | push: true 39 | tags: | 40 | ${{ steps.set_image_name.outputs.image_name }} 41 | cache-from: type=gha 42 | cache-to: type=gha,mode=max 43 | -------------------------------------------------------------------------------- /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | description: Run tests 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/setup-node@v2 7 | with: 8 | node-version: '16' 9 | cache: 'npm' 10 | cache-dependency-path: ./backend/package-lock.json 11 | - name: Install deps and run tests 12 | working-directory: backend 13 | shell: sh 14 | run: 15 | npm ci 16 | npm run test 17 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: Production deploy 2 | 3 | concurrency: 4 | group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}-prod' 5 | cancel-in-progress: true 6 | 7 | on: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | image_name: ${{ steps.build.outputs.image_name }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: ./.github/actions/build 19 | name: Build image 20 | id: build 21 | 22 | deploy_production: 23 | runs-on: ubuntu-latest 24 | environment: production 25 | needs: build 26 | container: 27 | image: qwolphin/kdsl:1.21.8 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | CHAIN_ID: 32 | - "1" 33 | - "137" 34 | - "43114" 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - name: GCP Auth 39 | uses: google-github-actions/auth@v0.4.0 40 | with: 41 | credentials_json: ${{ secrets.GCP_SA_KEY }} 42 | - name: Get GKE credentials 43 | uses: google-github-actions/get-gke-credentials@v0.4.0 44 | with: 45 | cluster_name: ${{ secrets.GKE_CLUSTER }} 46 | location: ${{ secrets.GKE_CLUSTER_REGION }} 47 | 48 | - name: Render kdsl resources into yaml 49 | env: 50 | MAINNET_RPC: '${{ secrets.MAINNET_RPC }}' 51 | POLYGON_RPC: '${{ secrets.POLYGON_RPC }}' 52 | RECIPE: "chain${{ matrix.CHAIN_ID }}" 53 | DOMAIN: cache-api-${{ matrix.CHAIN_ID }}.aave.com 54 | CHAIN_ID: ${{ matrix.CHAIN_ID }} 55 | IMAGE: ${{ needs.build.outputs.image_name }} 56 | COMMIT_SHA: "${{ github.sha }}" 57 | NAMESPACE: cache-${{ matrix.CHAIN_ID }} 58 | ENV_NAME: production 59 | run: | 60 | cd k8s/ 61 | kubectl config set-context --current --namespace=${{ env.NAMESPACE }} 62 | python3 main.py > ../rendered.yml 63 | 64 | - name: Apply k8s resources 65 | run: | 66 | kubectl apply -f rendered.yml --dry-run=server 67 | kubectl apply -f rendered.yml 68 | sleep 3 69 | kubectl wait --for condition=ready --timeout 90s pods -l "commit_sha=${{ github.sha }}" || \ 70 | { kubectl get pods && exit 1; } 71 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-close.yml: -------------------------------------------------------------------------------- 1 | name: Stop preview env 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | stop_dev: 9 | runs-on: ubuntu-latest 10 | environment: preview 11 | container: 12 | image: registry.gitlab.com/aave-tech/k8s:63f618c0 13 | credentials: 14 | username: github-actions 15 | password: ${{ secrets.KUBE_IMAGE_PULL }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | CHAIN_ID: ["1", "137", "43114"] 20 | steps: 21 | - name: Set k8s namespace 22 | shell: bash 23 | run: echo "NAMESPACE=cache-${NS_SUFFIX,,}" | tr -c '[:alnum:]-=\n' '-' >>${GITHUB_ENV} 24 | env: 25 | NS_SUFFIX: "${{ github.head_ref }}-${{ matrix.CHAIN_ID }}" 26 | 27 | - name: Remove preview env 28 | env: 29 | REF_NAME: '${{ github.head_ref }}' 30 | run: | 31 | mkdir -p ~/.kube 32 | echo "${{ secrets.DEV_KUBECONFIG }}" > ~/.kube/config 33 | kubectl config set-context --current --namespace="${{ env.NAMESPACE }}" 34 | kubectl delete deploy --all 35 | kubectl delete svc --all 36 | kubectl delete ingress --all 37 | kubectl delete ns "${{ env.NAMESPACE }}" 38 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: On Pull Request 2 | 3 | concurrency: 4 | group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}" 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | image_name: ${{ steps.build.outputs.image_name }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - uses: ./.github/actions/build 20 | name: Build image 21 | id: build 22 | 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - uses: ./.github/actions/test 29 | name: Run tests 30 | 31 | deploy_dev: 32 | runs-on: ubuntu-latest 33 | environment: preview 34 | needs: build 35 | container: 36 | image: qwolphin/kdsl:1.21.8 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | CHAIN_ID: ["1", "137", "43114"] 41 | steps: 42 | - uses: actions/checkout@v2 43 | 44 | - name: Set k8s namespace 45 | shell: bash 46 | run: | 47 | echo -e "NAMESPACE=cache-${NS_SUFFIX,,}\nENV_NAME=${{ github.head_ref }}" | tr -c '[:alnum:]-=\n_' '-' >> ${GITHUB_ENV} 48 | env: 49 | NS_SUFFIX: "${{ github.head_ref }}-${{ matrix.CHAIN_ID }}" 50 | 51 | - name: Render kdsl resources into yaml 52 | env: 53 | MAINNET_RPC: "${{ secrets.MAINNET_RPC }}" 54 | POLYGON_RPC: "${{ secrets.POLYGON_RPC }}" 55 | RECIPE: "chain${{ matrix.CHAIN_ID }}" 56 | DOMAIN: "${{ env.NAMESPACE }}.aaw.fi" 57 | CHAIN_ID: "${{ matrix.CHAIN_ID }}" 58 | IMAGE: "${{ needs.build.outputs.image_name }}" 59 | COMMIT_SHA: "${{ github.sha }}" 60 | run: | 61 | cd k8s/ 62 | python3 main.py > ../rendered.yml 63 | 64 | - name: Set up kubeconfig 65 | run: | 66 | mkdir -p ~/.kube 67 | echo "${{ secrets.DEV_KUBECONFIG }}" > ~/.kube/config 68 | kubectl config set-context --current --namespace="${{ env.NAMESPACE }}" 69 | 70 | - name: Apply k8s resources 71 | shell: bash 72 | run: | 73 | kubectl apply -f rendered.yml 74 | sleep 3 75 | kubectl wait --for condition=ready --timeout 90s pods -l "commit_sha=${{ github.sha }}" || \ 76 | { kubectl get pods && exit 1; } 77 | 78 | - uses: actions/github-script@v5 79 | if: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }} 80 | with: 81 | script: | 82 | github.rest.issues.createComment({ 83 | issue_number: context.payload.number, 84 | owner: context.repo.owner, 85 | repo: context.repo.repo, 86 | body: 'Preview link for chain ${{ matrix.CHAIN_ID }}: https://${{ env.NAMESPACE }}.aaw.fi/' 87 | }) 88 | -------------------------------------------------------------------------------- /.github/workflows/push-master.yml: -------------------------------------------------------------------------------- 1 | name: Tests on push to master 2 | 3 | concurrency: 4 | group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - 'master' 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: ./.github/actions/test 19 | name: Run tests 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /build 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # IDEs and editors 34 | .idea 35 | .project 36 | .classpath 37 | .c9/ 38 | *.launch 39 | .settings/ 40 | *.sublime-workspace 41 | 42 | # IDE - VSCode 43 | .vscode/* 44 | !.vscode/settings.json 45 | !.vscode/tasks.json 46 | !.vscode/launch.json 47 | !.vscode/extensions.json 48 | 49 | # misc 50 | .sass-cache 51 | connect.lock 52 | typings 53 | 54 | # Logs 55 | logs 56 | *.log 57 | npm-debug.log* 58 | yarn-debug.log* 59 | yarn-error.log* 60 | 61 | 62 | # Dependency directories 63 | node_modules/ 64 | jspm_packages/ 65 | 66 | # Optional npm cache directory 67 | .npm 68 | 69 | # Optional eslint cache 70 | .eslintcache 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variables file 82 | .env 83 | 84 | # next.js build output 85 | .next 86 | 87 | # Lerna 88 | lerna-debug.log 89 | 90 | # System Files 91 | .DS_Store 92 | Thumbs.db 93 | 94 | #k8s/render.sh 95 | __pycache__ 96 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to run 4 | 5 | ```bash 6 | docker-compose up 7 | ``` 8 | 9 | ## How to access the data 10 | 11 | [http://localhost:3000/graphql](http://localhost:3000/graphql) - to query data via graphql 12 | 13 | [ws://localhost:3000/graphql](ws://localhost:3000/graphql) - for subscription 14 | 15 | ## Environment parameters 16 | 17 | ### General 18 | 19 | PORT - the port for the api, default 3000 20 | REDIS_HOST - key-value storage url, default - "redis" 21 | 22 | ### Network and pools 23 | 24 | NETWORK - the blockchain network name 25 | PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES - the list of the pool addresses providers to support, splitted by comma 26 | POOL_UI_DATA_PROVIDER_ADDRESS - data aggregation helper contract address 27 | PROTOCOLS_WITH_INCENTIVES_ADDRESSES - the subset of the list of pool addresses providers which has incentives program, if any 28 | AAVE_TOKEN_ADDRESS - the address of the AAVE token (optional) 29 | ABPT_TOKEN - the address of the AAVE/ETH balancer pool token (optional) 30 | STK_AAVE_TOKEN_ADDRESS - the address of the STK AAVE token (optional) 31 | STK_ABPT_TOKEN_ADDRESS - the address of the STK AAVE/ETH balancer pool token (optional) 32 | STAKE_DATA_PROVIDER - The address of the stake helper data provider (optional) 33 | STAKE_DATA_POOLING_INTERVAL - The interval between stake data pooling (optional`) 34 | RPC_URL - JSON RPC endpoint url to access the data 35 | RPC_MAX_TIMEOUT - The maximum timeout in seconds for the RPC calls until it tries to use backup nodes 36 | BACKUP_RPC_URLS - A list of the backup nodes to use if the main goes down, splitted by comma 37 | 38 | ### Pooling 39 | 40 | STAKE_DATA_POOLING_INTERVAL - The interval in seconds between stake data pooling 41 | GENERAL_RESERVES_DATA_POOLING_INTERVAL - pooling interval for the general pool data updates, in seconds, default value - 1 42 | BLOCK_NUMBER_POOLING_INTERVAL - pooling interval for the block number updates, in seconds, default value - 1 43 | USERS_DATA_POOLING_INTERVAL - pooling interval for the users data updates, in seconds, default value - 1 44 | 45 | ## Testing 46 | 47 | We have high coverage on this repo due. To run tests run: 48 | 49 | ```bash 50 | $ npm test 51 | ``` 52 | 53 | Any new tasks, endpoints or services added should be tested. 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The 3-Clause BSD License 2 | 3 | Copyright 2021 Aave SAGL 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | .///. .///. //. .// `/////////////- 3 | `++:++` .++:++` :++` `++: `++:......---.` 4 | `/+: -+/` `++- :+/` /+/ `/+/ `++. 5 | /+/ :+/ /+: /+/ `/+/ /+/` `++. 6 | -::/++::` /+: -::/++::` `/+: `++: :++` `++/:::::::::. 7 | -:+++::-` `/+: --++/---` `++- .++- -++. `++/:::::::::. 8 | -++. .++- -++` .++. .++. .++- `++. 9 | .++- -++. .++. -++. -++``++- `++. 10 | `++: :++` .++- :++` :+//+: `++:----------` 11 | -/: :/- -/: :/. ://: `/////////////- 12 | ``` 13 | 14 | # Aave data caching server :ghost: 15 | 16 | Thin and simple data caching layer to give better "real time" experience to users of the open source [Aave interface](https://github.com/aave/aave-ui). 17 | 18 | The helper smart contracts can be found in [here](https://github.com/aave/protocol-v2/tree/master/contracts/misc) 19 | 20 | ## Current data pushing 21 | 22 | - Protocol data 23 | - Protocol user data 24 | - Stake general data 25 | - Stake user data 26 | 27 | ## Contribution 28 | 29 | For instructions on local deployment and configurations, see [Contributing](./CONTRIBUTING.md) 30 | 31 | ## License 32 | 33 | [BSD-3-Clause](./LICENSE.md) 34 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile* 2 | .gitignore 3 | docker-compose.* 4 | nodemon.json 5 | tslint.json 6 | .editorconfig 7 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 9 | root: true, 10 | env: { 11 | node: true, 12 | jest: true, 13 | }, 14 | ignorePatterns: ['.eslintrc.js'], 15 | rules: { 16 | // '@typescript-eslint/interface-name-prefix': 'off', 17 | // '@typescript-eslint/explicit-function-return-type': 'off', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | // '@typescript-eslint/no-explicit-any': 'off', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /build 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # IDEs and editors 34 | .idea 35 | .project 36 | .classpath 37 | .c9/ 38 | *.launch 39 | .settings/ 40 | *.sublime-workspace 41 | 42 | # IDE - VSCode 43 | .vscode/* 44 | !.vscode/settings.json 45 | !.vscode/tasks.json 46 | !.vscode/launch.json 47 | !.vscode/extensions.json 48 | 49 | # misc 50 | .sass-cache 51 | connect.lock 52 | typings 53 | 54 | # Logs 55 | logs 56 | *.log 57 | npm-debug.log* 58 | yarn-debug.log* 59 | yarn-error.log* 60 | 61 | 62 | # Dependency directories 63 | node_modules/ 64 | jspm_packages/ 65 | 66 | # Optional npm cache directory 67 | .npm 68 | 69 | # Optional eslint cache 70 | .eslintcache 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variables file 82 | .env 83 | 84 | # next.js build output 85 | .next 86 | 87 | # Lerna 88 | lerna-debug.log 89 | 90 | # System Files 91 | .DS_Store 92 | Thumbs.db 93 | -------------------------------------------------------------------------------- /backend/.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /app/ 4 | 5 | ADD package.json package-lock.json /app/ 6 | 7 | RUN npm ci 8 | 9 | ADD ./ /app/ 10 | 11 | ENV HOST 0.0.0.0 12 | EXPOSE 3000 13 | 14 | CMD ["npm", "run", "prod"] 15 | -------------------------------------------------------------------------------- /backend/__tests__/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { BACKUP_RPC_URLS, RPC_MAX_TIMEOUT, CONFIG, STAKING_CONFIG } from '../src/config'; 2 | 3 | describe('config - only tests on environment variables which parse to something else', () => { 4 | it('PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES should return an array', () => { 5 | expect(CONFIG.PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES).toBeInstanceOf(Array); 6 | expect(CONFIG.PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES).toHaveLength(2); 7 | }); 8 | 9 | it('BACKUP_RPC_URLS should return an array', () => { 10 | expect(BACKUP_RPC_URLS).toBeInstanceOf(Array); 11 | expect(BACKUP_RPC_URLS).toHaveLength(0); 12 | }); 13 | 14 | it('RPC_MAX_TIMEOUT should return a number', () => { 15 | expect(RPC_MAX_TIMEOUT).toEqual(1000); 16 | }); 17 | 18 | it('STAKE_DATA_POOLING_INTERVAL should return a number', () => { 19 | expect(STAKING_CONFIG.STAKE_DATA_POOLING_INTERVAL).toEqual(1000); 20 | }); 21 | 22 | it('BLOCK_NUMBER_POOLING_INTERVAL should return a number', () => { 23 | expect(CONFIG.BLOCK_NUMBER_POOLING_INTERVAL).toEqual(1000); 24 | }); 25 | 26 | it('GENERAL_RESERVES_DATA_POOLING_INTERVAL should return a number', () => { 27 | expect(CONFIG.GENERAL_RESERVES_DATA_POOLING_INTERVAL).toEqual(5000); 28 | }); 29 | 30 | it('USERS_DATA_POOLING_INTERVAL should return a number', () => { 31 | expect(CONFIG.USERS_DATA_POOLING_INTERVAL).toEqual(5000); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /backend/__tests__/custom-provider/aave-custom-provider.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ethersUtils from 'ethers/lib/utils'; 2 | import { BACKUP_RPC_URLS, RPC_MAX_TIMEOUT, RPC_URL } from '../../src/config'; 3 | import { generate } from '../../src/custom-provider/aave-provider-manager'; 4 | 5 | jest.mock('ethers/lib/utils', () => ({ 6 | __esModule: true, 7 | fetchJson: jest.fn().mockImplementation(() => Promise.resolve('0x01')), 8 | })); 9 | 10 | const buildNoBackupDefaultProvider = generate({ 11 | selectedNode: RPC_URL, 12 | backupNodes: BACKUP_RPC_URLS, 13 | maxTimout: RPC_MAX_TIMEOUT, 14 | }); 15 | 16 | const buildBackupDefaultProvider = generate({ 17 | selectedNode: RPC_URL, 18 | backupNodes: ['BACKUP_NODE'], 19 | maxTimout: RPC_MAX_TIMEOUT, 20 | }); 21 | 22 | const buildUsingBackupDefaultProvider = ( 23 | mainNodeReconnectionContext: { 24 | reconnectAttempts: number; 25 | lastAttempted: number; 26 | } = undefined 27 | ) => { 28 | return generate({ 29 | selectedNode: 'BACKUP_NODE', 30 | // @ts-ignore 31 | alreadyUsedNodes: [{ node: RPC_URL, wasMainNode: true }], 32 | mainNode: RPC_URL, 33 | mainNodeReconnectionContext, 34 | backupNodes: [], 35 | maxTimout: RPC_MAX_TIMEOUT, 36 | }); 37 | }; 38 | 39 | describe('AaveCustomProvider', () => { 40 | beforeEach(() => { 41 | jest 42 | .spyOn(ethersUtils, 'fetchJson') 43 | .mockImplementation(() => Promise.resolve({ result: '0x01' })); 44 | }); 45 | describe('send', () => { 46 | it('should send jsonrpc message correctly', async () => { 47 | const result = await buildNoBackupDefaultProvider.send('eth_blockNumber', []); 48 | 49 | expect(result).not.toBeUndefined(); 50 | }); 51 | 52 | it('should call fetch from ethers utils', async () => { 53 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 54 | await buildNoBackupDefaultProvider.send('eth_blockNumber', []); 55 | 56 | expect(spy).toHaveBeenCalledTimes(1); 57 | }); 58 | 59 | it('if fetch throws a valid node error throw error', async () => { 60 | const spy = jest.spyOn(ethersUtils, 'fetchJson').mockImplementation(() => { 61 | throw new Error('NODE_ERROR'); 62 | }); 63 | await expect(buildNoBackupDefaultProvider.send('eth_blockNumber', [])).rejects.toThrow( 64 | 'NODE_ERROR' 65 | ); 66 | 67 | expect(spy).toHaveBeenCalledTimes(1); 68 | }); 69 | 70 | it('if fetch throws a invalid node error it should try to switch nodes but throw that error if it can not find any nodes to switch to', async () => { 71 | const spy = jest.spyOn(ethersUtils, 'fetchJson').mockImplementation(() => { 72 | throw new Error('missing response'); 73 | }); 74 | await expect(buildNoBackupDefaultProvider.send('eth_blockNumber', [])).rejects.toThrow( 75 | 'missing response' 76 | ); 77 | 78 | expect(spy).toHaveBeenCalledTimes(1); 79 | }); 80 | 81 | it('if fetch throws a invalid node error it should switch nodes successfully', async () => { 82 | const spy = jest.spyOn(ethersUtils, 'fetchJson').mockImplementationOnce(() => { 83 | throw new Error('missing response'); 84 | }); 85 | const result = await buildBackupDefaultProvider.send('eth_blockNumber', []); 86 | 87 | expect(result).not.toBeUndefined(); 88 | expect(spy).toHaveBeenCalledTimes(2); 89 | }); 90 | 91 | it('should not switch back to main node if it mainNodeReconnectionContext is not defined', async () => { 92 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 93 | 94 | const result = await buildUsingBackupDefaultProvider().send('eth_blockNumber', []); 95 | expect(result).not.toBeUndefined(); 96 | expect(spy).toHaveBeenCalledTimes(1); 97 | // @ts-ignore 98 | expect(spy.mock.calls[0][0].url).toEqual('BACKUP_NODE'); 99 | }); 100 | 101 | it('should switch back to main node if it hits the config requirements first threshold', async () => { 102 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 103 | 104 | const result = await buildUsingBackupDefaultProvider({ 105 | reconnectAttempts: 5, 106 | lastAttempted: 0, 107 | }).send('eth_blockNumber', []); 108 | expect(result).not.toBeUndefined(); 109 | expect(spy).toHaveBeenCalledTimes(1); 110 | // @ts-ignore 111 | expect(spy.mock.calls[0][0].url).toEqual(RPC_URL); 112 | }); 113 | 114 | it('should switch back to main node if it hits the config requirements second threshold', async () => { 115 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 116 | 117 | const result = await buildUsingBackupDefaultProvider({ 118 | reconnectAttempts: 6, 119 | lastAttempted: 0, 120 | }).send('eth_blockNumber', []); 121 | expect(result).not.toBeUndefined(); 122 | expect(spy).toHaveBeenCalledTimes(1); 123 | // @ts-ignore 124 | expect(spy.mock.calls[0][0].url).toEqual(RPC_URL); 125 | }); 126 | 127 | it('should switch back to main node if it hits the config requirements thrid threshold', async () => { 128 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 129 | 130 | const result = await buildUsingBackupDefaultProvider({ 131 | reconnectAttempts: 11, 132 | lastAttempted: 0, 133 | }).send('eth_blockNumber', []); 134 | expect(result).not.toBeUndefined(); 135 | expect(spy).toHaveBeenCalledTimes(1); 136 | // @ts-ignore 137 | expect(spy.mock.calls[0][0].url).toEqual(RPC_URL); 138 | }); 139 | 140 | it('should switch back to main node if it hits the config requirements fourth threshold', async () => { 141 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 142 | 143 | const result = await buildUsingBackupDefaultProvider({ 144 | reconnectAttempts: 21, 145 | lastAttempted: 0, 146 | }).send('eth_blockNumber', []); 147 | expect(result).not.toBeUndefined(); 148 | expect(spy).toHaveBeenCalledTimes(1); 149 | // @ts-ignore 150 | expect(spy.mock.calls[0][0].url).toEqual(RPC_URL); 151 | }); 152 | 153 | it('should switch back to main node if it hits the config requirements fifth threshold', async () => { 154 | const spy = jest.spyOn(ethersUtils, 'fetchJson'); 155 | 156 | const result = await buildUsingBackupDefaultProvider({ 157 | reconnectAttempts: Infinity, 158 | lastAttempted: 0, 159 | }).send('eth_blockNumber', []); 160 | expect(result).not.toBeUndefined(); 161 | expect(spy).toHaveBeenCalledTimes(1); 162 | // @ts-ignore 163 | expect(spy.mock.calls[0][0].url).toEqual(RPC_URL); 164 | }); 165 | }); 166 | 167 | describe('getResult', () => { 168 | it('should throw error if payload has error in', () => { 169 | expect(() => { 170 | // @ts-ignore 171 | buildNoBackupDefaultProvider.getResult({ error: { message: 'blah' } }); 172 | }).toThrowError('blah'); 173 | }); 174 | 175 | it('should return result if no error present', () => { 176 | // @ts-ignore 177 | const results = buildNoBackupDefaultProvider.getResult({ result: '0x01' }); 178 | expect(results).toBe('0x01'); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /backend/__tests__/custom-provider/aave-provider-manager.spec.ts: -------------------------------------------------------------------------------- 1 | import { BACKUP_RPC_URLS, RPC_MAX_TIMEOUT, RPC_URL } from '../../src/config'; 2 | import { AaveCustomProvider } from '../../src/custom-provider/aave-custom-provider'; 3 | import { generate } from '../../src/custom-provider/aave-provider-manager'; 4 | 5 | describe('AaveProviderManager', () => { 6 | describe('generate', () => { 7 | it('should generate a new `AaveCustomProvider`', () => { 8 | const provider = generate({ 9 | selectedNode: RPC_URL, 10 | backupNodes: BACKUP_RPC_URLS, 11 | maxTimout: RPC_MAX_TIMEOUT, 12 | }); 13 | 14 | expect(provider).toBeInstanceOf(AaveCustomProvider); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /backend/__tests__/graphql/validators/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { validateSync } from 'class-validator'; 2 | import { IsEthAddress } from '../../../src/graphql/validators'; 3 | import { poolAddress } from '../../mocks'; 4 | 5 | class Test { 6 | @IsEthAddress() 7 | poolAddress: string; 8 | } 9 | 10 | xdescribe('Validators', () => { 11 | describe('IsEthAddress', () => { 12 | it('should register errors as ethereum address is incorrect', () => { 13 | const test = new Test(); 14 | test.poolAddress = 'sdfsda'; 15 | const errors = validateSync(test); 16 | expect(errors.length).toEqual(1); 17 | }); 18 | 19 | it('should validate successfully with no errors', () => { 20 | const test = new Test(); 21 | test.poolAddress = poolAddress; 22 | const errors = validateSync(test); 23 | expect(errors.length).toEqual(0); 24 | }); 25 | 26 | it('should validate successfully with no errors', () => { 27 | const test = new Test(); 28 | test.poolAddress = '0x' + poolAddress.substring(2).toUpperCase(); 29 | const errors = validateSync(test); 30 | expect(errors.length).toEqual(0); 31 | }); 32 | 33 | it('should validate successfully with no errors', () => { 34 | const test = new Test(); 35 | test.poolAddress = poolAddress.toLowerCase(); 36 | const errors = validateSync(test); 37 | expect(errors.length).toEqual(0); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /backend/__tests__/helpers/ethereum.spec.ts: -------------------------------------------------------------------------------- 1 | import { providers } from 'ethers'; 2 | import { 3 | alchemyWeb3Provider, 4 | ethereumProvider, 5 | getBlockNumber, 6 | getBlockNumberRpc, 7 | getUsersFromLogs, 8 | } from '../../src/helpers/ethereum'; 9 | import * as redis from '../../src/redis'; 10 | 11 | jest.mock('../../src/redis', () => ({ 12 | __esModule: true, 13 | getBlockNumberRedis: jest.fn(), 14 | setBlockNumberRedis: jest.fn(), 15 | })); 16 | 17 | describe('ethereum', () => { 18 | describe('ethereumProvider', () => { 19 | // when we start using the custom provider then add this test back in 20 | // xit('should be instance of `AaveCustomProvider`', () => { 21 | // expect(ethereumProvider).toBeInstanceOf(AaveCustomProvider); 22 | // }); 23 | 24 | it('should be instance of `JsonRpcProvider`', () => { 25 | expect(ethereumProvider).toBeInstanceOf(providers.StaticJsonRpcProvider); 26 | }); 27 | }); 28 | 29 | describe('alchemyWeb3Provider', () => { 30 | it('should be instance of `AlchemyWeb3`', () => { 31 | expect(alchemyWeb3Provider.eth).not.toBeUndefined(); 32 | }); 33 | }); 34 | 35 | describe('getBlockNumberRpc', () => { 36 | it('should return block number from node', async () => { 37 | const spy = jest.spyOn(ethereumProvider, 'getBlockNumber').mockImplementation(async () => 1); 38 | const blockNumber = await getBlockNumberRpc(); 39 | expect(blockNumber).toEqual(1); 40 | expect(spy).toHaveBeenCalledTimes(1); 41 | }); 42 | 43 | it('should return 0 if getBlockNumber throws an error', async () => { 44 | const spy = jest.spyOn(ethereumProvider, 'getBlockNumber').mockImplementation(async () => { 45 | throw new Error('error'); 46 | }); 47 | const blockNumber = await getBlockNumberRpc(); 48 | expect(blockNumber).toEqual(0); 49 | expect(spy).toHaveBeenCalledTimes(1); 50 | }); 51 | }); 52 | 53 | describe('getBlockNumber', () => { 54 | it('should not call redis cache if useCache is false and call rpc method', async () => { 55 | const getBlockNumberSpy = jest 56 | .spyOn(ethereumProvider, 'getBlockNumber') 57 | .mockImplementation(async () => 1); 58 | const redisGetSpy = jest.spyOn(redis, 'getBlockNumberRedis'); 59 | const blockNumber = await getBlockNumber(false); 60 | expect(blockNumber).toEqual(1); 61 | expect(getBlockNumberSpy).toHaveBeenCalledTimes(1); 62 | expect(redisGetSpy).toHaveBeenCalledTimes(0); 63 | }); 64 | 65 | it('should call redis cache if useCache is true and call rpc method', async () => { 66 | const getBlockNumberSpy = jest 67 | .spyOn(ethereumProvider, 'getBlockNumber') 68 | .mockImplementation(async () => 1); 69 | const redisGetSpy = jest.spyOn(redis, 'getBlockNumberRedis'); 70 | const blockNumber = await getBlockNumber(true); 71 | expect(blockNumber).toEqual(1); 72 | expect(getBlockNumberSpy).toHaveBeenCalledTimes(1); 73 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 74 | }); 75 | 76 | it('should call redis cache if useCache is true and not call rpc method', async () => { 77 | const getBlockNumberSpy = jest 78 | .spyOn(ethereumProvider, 'getBlockNumber') 79 | .mockImplementation(async () => 1); 80 | const redisGetSpy = jest 81 | .spyOn(redis, 'getBlockNumberRedis') 82 | .mockImplementation(async () => '1'); 83 | const blockNumber = await getBlockNumber(true); 84 | expect(blockNumber).toEqual(1); 85 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 86 | expect(getBlockNumberSpy).toHaveBeenCalledTimes(0); 87 | }); 88 | }); 89 | 90 | describe('getUsersFromLogs', () => { 91 | it('should return transfer events for the contract', async () => { 92 | const getPastLogsSpy = jest.spyOn(alchemyWeb3Provider.eth, 'getPastLogs'); 93 | const decodeLog = jest.spyOn(alchemyWeb3Provider.eth.abi, 'decodeLog'); 94 | 95 | const users = await getUsersFromLogs( 96 | ['0x4da27a545c0c5b758a6ba100e3a049001de870f5'], 97 | 12964000, 98 | 12964252 99 | ); 100 | expect(users.length).toEqual(5); 101 | 102 | expect(getPastLogsSpy).toHaveBeenCalledTimes(1); 103 | expect(getPastLogsSpy).toHaveBeenCalledWith({ 104 | address: ['0x4da27a545c0c5b758a6ba100e3a049001de870f5'], 105 | fromBlock: '0xc5d0a0', 106 | toBlock: '0xc5d19c', 107 | topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'], 108 | }); 109 | 110 | // only 4 raw logs but remember it sends to to and from 111 | expect(decodeLog).toHaveBeenCalledTimes(4); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /backend/__tests__/helpers/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { createHash, jsonParse, sleep } from '../../src/helpers/utils'; 2 | 3 | describe('utils', () => { 4 | describe('sleep', () => { 5 | it('should sleep for 1000 seconds', async () => { 6 | const timestamp = new Date().getTime(); 7 | await sleep(1000); 8 | const timestamp2 = new Date().getTime(); 9 | expect(timestamp2 - timestamp).toBeGreaterThan(999); 10 | 11 | // process of new date may add some time! 12 | expect(timestamp2 - timestamp).toBeLessThan(1100); 13 | }); 14 | }); 15 | 16 | describe('jsonParse', () => { 17 | it('should parse json', () => { 18 | const parsed = jsonParse<{ foo: boolean }>(JSON.stringify({ foo: true })); 19 | expect(parsed).toEqual({ foo: true }); 20 | }); 21 | }); 22 | 23 | describe('createHash', () => { 24 | it('should hash correctly', () => { 25 | const parsed = createHash({ foo: true, boo: false }); 26 | expect(parsed).toEqual('ZcydfSyD+S/1ljPdDYKFXA=='); 27 | }); 28 | }); 29 | }); 30 | 31 | // xit('helper test to use when debugging', async () => { 32 | // // expect(utils.id('RewardsClaimed(address,address,uint256)')).toEqual(0); 33 | // //expect(utils.id('RewardsClaimed(address,address,address,uint256)')).toEqual(0); 34 | 35 | // // const result = await getUsersFromLogs( 36 | // // ['0x357D51124f59836DeD84c8a1730D72B749d8BC23'], 37 | // // 18695605, 38 | // // 18695606, 39 | // // ['RewardsClaimed(address,address,address,uint256)'] 40 | // // ); 41 | 42 | // const alchemyWeb3Provider = createAlchemyWeb3( 43 | // 'xxxxx' 44 | // ); 45 | 46 | // const topics = ['RewardsClaimed(address,address,address,uint256)']; 47 | 48 | // const rawLogs = await alchemyWeb3Provider.eth.getPastLogs({ 49 | // fromBlock: 18696158, 50 | // toBlock: 18696158, 51 | // topics: topics.map((t) => utils.id(t)), 52 | // address: ['0x357D51124f59836DeD84c8a1730D72B749d8BC23'], 53 | // }); 54 | // const users: string[] = []; 55 | // rawLogs.forEach((data) => { 56 | // const logs = alchemyWeb3Provider.eth.abi.decodeLog( 57 | // [ 58 | // { 59 | // type: 'address', 60 | // name: 'from', 61 | // indexed: true, 62 | // }, 63 | // { 64 | // type: 'address', 65 | // name: 'to', 66 | // indexed: true, 67 | // }, 68 | // ], 69 | // '', 70 | // [data.topics[1], data.topics[2]] 71 | // ); 72 | 73 | // users.push(logs.from); 74 | // }); 75 | 76 | // expect(users).toEqual(true); 77 | // }); 78 | -------------------------------------------------------------------------------- /backend/__tests__/mocks.ts: -------------------------------------------------------------------------------- 1 | export const poolAddress = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5'; 2 | export const userAddress = '0x45Cd08334aeedd8a06265B2Ae302E3597d8fAA28'; 3 | -------------------------------------------------------------------------------- /backend/__tests__/services/incentives-data/provider.spec.ts: -------------------------------------------------------------------------------- 1 | import * as redis from '../../../src/redis'; 2 | import { 3 | getPoolIncentives, 4 | getUserPoolIncentives, 5 | } from '../../../src/services/incentives-data/provider'; 6 | import * as rpc from '../../../src/services/incentives-data/rpc'; 7 | 8 | jest.mock('../../../src/redis', () => ({ 9 | __esModule: true, 10 | getPoolIncentivesDataRedis: jest.fn(), 11 | setPoolIncentivesDataRedis: jest.fn(), 12 | getPoolIncentivesUserDataRedis: jest.fn(), 13 | setPoolIncentivesUserDataRedis: jest.fn(), 14 | })); 15 | 16 | const getPoolIncentivesRPCMockResponse = 'mockedGetProtocolDataRPC'; 17 | const getPoolIncentivesUserDataRPCMockResponse = { 18 | data: 'mockedGetProtocolDataRPC', 19 | hash: 'vCQEpPWzvsdKk6A6A9jFfg==', 20 | }; 21 | 22 | jest.mock('../../../src/services/incentives-data/rpc', () => ({ 23 | __esModule: true, 24 | getPoolIncentivesRPC: jest.fn().mockImplementation(() => getPoolIncentivesRPCMockResponse), 25 | getUserPoolIncentivesRPC: jest 26 | .fn() 27 | .mockImplementation(() => getPoolIncentivesUserDataRPCMockResponse), 28 | })); 29 | 30 | describe('provider', () => { 31 | const lendingPoolAddressProvider = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5'; 32 | const userAddress = '0x45Cd08334aeedd8a06265B2Ae302E3597d8fAA28'; 33 | const incentivesKey = `incentives-${lendingPoolAddressProvider}`; 34 | 35 | describe('getPoolIncentivesData', () => { 36 | it('should return no value from redis and call rpc method', async () => { 37 | const redisGetSpy = jest.spyOn(redis, 'getPoolIncentivesDataRedis'); 38 | const redisSetSpy = jest.spyOn(redis, 'setPoolIncentivesDataRedis'); 39 | 40 | const rpcSpy = jest.spyOn(rpc, 'getPoolIncentivesRPC'); 41 | 42 | const result = await getPoolIncentives(lendingPoolAddressProvider); 43 | expect(result).toEqual(getPoolIncentivesRPCMockResponse); 44 | 45 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 46 | expect(redisGetSpy).toHaveBeenCalledWith(incentivesKey); 47 | 48 | expect(rpcSpy).toHaveBeenCalledTimes(1); 49 | expect(rpcSpy).toHaveBeenCalledWith(lendingPoolAddressProvider); 50 | 51 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 52 | expect(redisSetSpy).toHaveBeenCalledWith( 53 | incentivesKey, 54 | getPoolIncentivesUserDataRPCMockResponse 55 | ); 56 | }); 57 | 58 | it('should get value from redis and not call rpc method', async () => { 59 | const redisGetSpy = jest 60 | .spyOn(redis, 'getPoolIncentivesDataRedis') 61 | .mockImplementationOnce(async () => getPoolIncentivesUserDataRPCMockResponse as any); 62 | const redisSetSpy = jest.spyOn(redis, 'setPoolIncentivesDataRedis'); 63 | 64 | const rpcSpy = jest.spyOn(rpc, 'getPoolIncentivesRPC'); 65 | 66 | const result = await getPoolIncentives(lendingPoolAddressProvider); 67 | expect(result).toEqual(getPoolIncentivesRPCMockResponse); 68 | 69 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 70 | expect(redisGetSpy).toHaveBeenCalledWith(incentivesKey); 71 | 72 | expect(rpcSpy).toHaveBeenCalledTimes(0); 73 | 74 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 75 | }); 76 | }); 77 | describe('getUserPoolIncentives', () => { 78 | it('should not call cache if cacheFirst is passed in as false', async () => { 79 | const redisGetSpy = jest.spyOn(redis, 'getPoolIncentivesUserDataRedis'); 80 | const redisSetSpy = jest.spyOn(redis, 'setPoolIncentivesUserDataRedis'); 81 | 82 | const rpcSpy = jest.spyOn(rpc, 'getUserPoolIncentivesRPC'); 83 | 84 | const result = await getUserPoolIncentives(lendingPoolAddressProvider, userAddress, false); 85 | expect(result).toEqual(getPoolIncentivesUserDataRPCMockResponse); 86 | 87 | expect(redisGetSpy).toHaveBeenCalledTimes(0); 88 | 89 | expect(rpcSpy).toHaveBeenCalledTimes(1); 90 | expect(rpcSpy).toHaveBeenCalledWith(lendingPoolAddressProvider, userAddress); 91 | 92 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 93 | expect(redisSetSpy).toHaveBeenCalledWith( 94 | incentivesKey, 95 | userAddress, 96 | getPoolIncentivesUserDataRPCMockResponse 97 | ); 98 | }); 99 | 100 | it('should call cache if cacheFirst is passed in as true (empty cache path)', async () => { 101 | const redisGetSpy = jest.spyOn(redis, 'getPoolIncentivesUserDataRedis'); 102 | const redisSetSpy = jest.spyOn(redis, 'setPoolIncentivesUserDataRedis'); 103 | 104 | const rpcSpy = jest.spyOn(rpc, 'getUserPoolIncentivesRPC'); 105 | 106 | const result = await getUserPoolIncentives(lendingPoolAddressProvider, userAddress, true); 107 | expect(result).toEqual(getPoolIncentivesUserDataRPCMockResponse); 108 | 109 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 110 | expect(redisGetSpy).toHaveBeenCalledWith(incentivesKey, userAddress); 111 | 112 | expect(rpcSpy).toHaveBeenCalledTimes(1); 113 | expect(rpcSpy).toHaveBeenCalledWith(lendingPoolAddressProvider, userAddress); 114 | 115 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 116 | expect(redisSetSpy).toHaveBeenCalledWith( 117 | incentivesKey, 118 | userAddress, 119 | getPoolIncentivesUserDataRPCMockResponse 120 | ); 121 | }); 122 | 123 | it('should call cache if cacheFirst is passed in as true (cache defined path)', async () => { 124 | const redisGetSpy = jest 125 | .spyOn(redis, 'getPoolIncentivesUserDataRedis') 126 | .mockImplementationOnce(async () => getPoolIncentivesUserDataRPCMockResponse as any); 127 | const redisSetSpy = jest.spyOn(redis, 'setPoolIncentivesUserDataRedis'); 128 | 129 | const rpcSpy = jest.spyOn(rpc, 'getUserPoolIncentivesRPC'); 130 | 131 | const result = await getUserPoolIncentives(lendingPoolAddressProvider, userAddress, true); 132 | expect(result).toEqual(getPoolIncentivesUserDataRPCMockResponse); 133 | 134 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 135 | expect(redisGetSpy).toHaveBeenCalledWith(incentivesKey, userAddress); 136 | 137 | expect(rpcSpy).toHaveBeenCalledTimes(0); 138 | 139 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /backend/__tests__/services/incentives-data/rpc.spec.ts: -------------------------------------------------------------------------------- 1 | import { ReserveIncentivesData } from '../../../src/graphql/object-types/incentives'; 2 | import { UserIncentivesData } from '../../../src/graphql/object-types/user-incentives'; 3 | import { 4 | getPoolIncentivesRPC, 5 | getUserPoolIncentivesRPC, 6 | } from '../../../src/services/incentives-data/rpc'; 7 | import { poolAddress, userAddress } from '../../mocks'; 8 | 9 | describe('rpc', () => { 10 | describe('getPoolIncentivesRPC', () => { 11 | it('should return `Incentives Data`', async () => { 12 | const result: ReserveIncentivesData[] = await getPoolIncentivesRPC({ 13 | lendingPoolAddressProvider: poolAddress, 14 | }); 15 | expect(result).toBeInstanceOf(Array); 16 | const response = result[0]; 17 | expect(response).toBeInstanceOf(Object); 18 | expect(response.aIncentiveData).toBeInstanceOf(Object); 19 | expect(response.vIncentiveData).toBeInstanceOf(Object); 20 | expect(response.sIncentiveData).toBeInstanceOf(Object); 21 | }); 22 | }); 23 | 24 | describe('getUserPoolIncentivesRPC', () => { 25 | jest.setTimeout(20000); 26 | it('should return `User Incentives Data`', async () => { 27 | const result: UserIncentivesData[] = await getUserPoolIncentivesRPC(poolAddress, userAddress); 28 | expect(result).toBeInstanceOf(Array); 29 | const response = result[0]; 30 | expect(response).toBeInstanceOf(Object); 31 | expect(response.aTokenIncentivesUserData).toBeInstanceOf(Object); 32 | expect(response.vTokenIncentivesUserData).toBeInstanceOf(Object); 33 | expect(response.sTokenIncentivesUserData).toBeInstanceOf(Object); 34 | expect(typeof response.aTokenIncentivesUserData.incentiveControllerAddress).toEqual('string'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /backend/__tests__/services/pool-data/provider.spec.ts: -------------------------------------------------------------------------------- 1 | import * as redis from '../../../src/redis'; 2 | import { getProtocolData, getProtocolUserData } from '../../../src/services/pool-data/provider'; 3 | import * as rpc from '../../../src/services/pool-data/rpc'; 4 | 5 | jest.mock('../../../src/redis', () => ({ 6 | __esModule: true, 7 | getProtocolDataRedis: jest.fn(), 8 | setProtocolDataRedis: jest.fn(), 9 | getProtocolUserDataRedis: jest.fn(), 10 | setProtocolUserDataRedis: jest.fn(), 11 | })); 12 | 13 | const getProtocolDataRPCMockResponse = 'mockedGetProtocolDataRPC'; 14 | const getProtocolUserDataRPCMockResponse = { 15 | data: 'mockedGetProtocolDataRPC', 16 | hash: 'vCQEpPWzvsdKk6A6A9jFfg==', 17 | }; 18 | 19 | jest.mock('../../../src/services/pool-data/rpc', () => ({ 20 | __esModule: true, 21 | getProtocolDataRPC: jest.fn().mockImplementation(() => getProtocolDataRPCMockResponse), 22 | getProtocolUserDataRPC: jest.fn().mockImplementation(() => getProtocolUserDataRPCMockResponse), 23 | })); 24 | 25 | describe('provider', () => { 26 | const poolAddress = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5'; 27 | const userAddress = '0x45Cd08334aeedd8a06265B2Ae302E3597d8fAA28'; 28 | 29 | describe('getProtocolData', () => { 30 | it('should return no value from redis and call rpc method', async () => { 31 | const redisGetSpy = jest 32 | .spyOn(redis, 'getProtocolDataRedis') 33 | .mockImplementationOnce(async () => null); 34 | const redisSetSpy = jest.spyOn(redis, 'setProtocolDataRedis'); 35 | 36 | const rpcSpy = jest.spyOn(rpc, 'getProtocolDataRPC'); 37 | 38 | const result = await getProtocolData(poolAddress); 39 | expect(result).toEqual(getProtocolDataRPCMockResponse); 40 | 41 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 42 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress); 43 | 44 | expect(rpcSpy).toHaveBeenCalledTimes(1); 45 | expect(rpcSpy).toHaveBeenCalledWith(poolAddress); 46 | 47 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 48 | expect(redisSetSpy).toHaveBeenCalledWith(poolAddress, getProtocolUserDataRPCMockResponse); 49 | }); 50 | 51 | it('should get value from redis and not call rpc method', async () => { 52 | const redisGetSpy = jest 53 | .spyOn(redis, 'getProtocolDataRedis') 54 | .mockImplementationOnce(async () => getProtocolUserDataRPCMockResponse as any); 55 | const redisSetSpy = jest.spyOn(redis, 'setProtocolDataRedis'); 56 | 57 | const rpcSpy = jest.spyOn(rpc, 'getProtocolDataRPC'); 58 | 59 | const result = await getProtocolData(poolAddress); 60 | expect(result).toEqual(getProtocolDataRPCMockResponse); 61 | 62 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 63 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress); 64 | 65 | expect(rpcSpy).toHaveBeenCalledTimes(0); 66 | 67 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 68 | }); 69 | }); 70 | 71 | describe('getProtocolUserData', () => { 72 | it('should not call cache if cacheFirst is passed in as false', async () => { 73 | const redisGetSpy = jest.spyOn(redis, 'getProtocolUserDataRedis'); 74 | const redisSetSpy = jest.spyOn(redis, 'setProtocolUserDataRedis'); 75 | 76 | const rpcSpy = jest.spyOn(rpc, 'getProtocolUserDataRPC'); 77 | 78 | const result = await getProtocolUserData(poolAddress, userAddress, false); 79 | expect(result).toEqual(getProtocolUserDataRPCMockResponse); 80 | 81 | expect(redisGetSpy).toHaveBeenCalledTimes(0); 82 | 83 | expect(rpcSpy).toHaveBeenCalledTimes(1); 84 | expect(rpcSpy).toHaveBeenCalledWith(poolAddress, userAddress); 85 | 86 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 87 | expect(redisSetSpy).toHaveBeenCalledWith( 88 | poolAddress, 89 | userAddress, 90 | getProtocolUserDataRPCMockResponse 91 | ); 92 | }); 93 | 94 | it('should call cache if cacheFirst is passed in as true (empty cache path)', async () => { 95 | const redisGetSpy = jest.spyOn(redis, 'getProtocolUserDataRedis'); 96 | const redisSetSpy = jest.spyOn(redis, 'setProtocolUserDataRedis'); 97 | 98 | const rpcSpy = jest.spyOn(rpc, 'getProtocolUserDataRPC'); 99 | 100 | const result = await getProtocolUserData(poolAddress, userAddress, true); 101 | expect(result).toEqual(getProtocolUserDataRPCMockResponse); 102 | 103 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 104 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress, userAddress); 105 | 106 | expect(rpcSpy).toHaveBeenCalledTimes(1); 107 | expect(rpcSpy).toHaveBeenCalledWith(poolAddress, userAddress); 108 | 109 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 110 | expect(redisSetSpy).toHaveBeenCalledWith( 111 | poolAddress, 112 | userAddress, 113 | getProtocolUserDataRPCMockResponse 114 | ); 115 | }); 116 | 117 | it('should call cache if cacheFirst is passed in as true (cache defined path)', async () => { 118 | const redisGetSpy = jest 119 | .spyOn(redis, 'getProtocolUserDataRedis') 120 | .mockImplementationOnce(async () => getProtocolUserDataRPCMockResponse as any); 121 | const redisSetSpy = jest.spyOn(redis, 'setProtocolUserDataRedis'); 122 | 123 | const rpcSpy = jest.spyOn(rpc, 'getProtocolUserDataRPC'); 124 | 125 | const result = await getProtocolUserData(poolAddress, userAddress, true); 126 | expect(result).toEqual(getProtocolUserDataRPCMockResponse); 127 | 128 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 129 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress, userAddress); 130 | 131 | expect(rpcSpy).toHaveBeenCalledTimes(0); 132 | 133 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /backend/__tests__/services/pool-data/provier.spec.ts: -------------------------------------------------------------------------------- 1 | import * as redis from '../../../src/redis'; 2 | import { getProtocolData, getProtocolUserData } from '../../../src/services/pool-data/provider'; 3 | import * as rpc from '../../../src/services/pool-data/rpc'; 4 | 5 | jest.mock('../../../src/redis', () => ({ 6 | __esModule: true, 7 | getProtocolDataRedis: jest.fn(), 8 | setProtocolDataRedis: jest.fn(), 9 | getProtocolUserDataRedis: jest.fn(), 10 | setProtocolUserDataRedis: jest.fn(), 11 | })); 12 | 13 | const getProtocolDataRPCMockResponse = 'mockedGetProtocolDataRPC'; 14 | const getProtocolUserDataRPCMockResponse = { 15 | data: 'mockedGetProtocolDataRPC', 16 | hash: 'vCQEpPWzvsdKk6A6A9jFfg==', 17 | }; 18 | 19 | jest.mock('../../../src/services/pool-data/rpc', () => ({ 20 | __esModule: true, 21 | getProtocolDataRPC: jest.fn().mockImplementation(() => getProtocolDataRPCMockResponse), 22 | getProtocolUserDataRPC: jest.fn().mockImplementation(() => getProtocolUserDataRPCMockResponse), 23 | })); 24 | 25 | describe('provider', () => { 26 | const poolAddress = '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5'; 27 | const userAddress = '0x45Cd08334aeedd8a06265B2Ae302E3597d8fAA28'; 28 | 29 | describe('getProtocolData', () => { 30 | it('should return no value from redis and call rpc method', async () => { 31 | const redisGetSpy = jest.spyOn(redis, 'getProtocolDataRedis'); 32 | const redisSetSpy = jest.spyOn(redis, 'setProtocolDataRedis'); 33 | 34 | const rpcSpy = jest.spyOn(rpc, 'getProtocolDataRPC'); 35 | 36 | const result = await getProtocolData(poolAddress); 37 | expect(result).toEqual(getProtocolDataRPCMockResponse); 38 | 39 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 40 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress); 41 | 42 | expect(rpcSpy).toHaveBeenCalledTimes(1); 43 | expect(rpcSpy).toHaveBeenCalledWith(poolAddress); 44 | 45 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 46 | expect(redisSetSpy).toHaveBeenCalledWith(poolAddress, getProtocolUserDataRPCMockResponse); 47 | }); 48 | 49 | it('should get value from redis and not call rpc method', async () => { 50 | const redisGetSpy = jest 51 | .spyOn(redis, 'getProtocolDataRedis') 52 | .mockImplementationOnce(async () => getProtocolUserDataRPCMockResponse as any); 53 | const redisSetSpy = jest.spyOn(redis, 'setProtocolDataRedis'); 54 | 55 | const rpcSpy = jest.spyOn(rpc, 'getProtocolDataRPC'); 56 | 57 | const result = await getProtocolData(poolAddress); 58 | expect(result).toEqual(getProtocolDataRPCMockResponse); 59 | 60 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 61 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress); 62 | 63 | expect(rpcSpy).toHaveBeenCalledTimes(0); 64 | 65 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 66 | }); 67 | }); 68 | 69 | describe('getProtocolUserData', () => { 70 | it('should not call cache if cacheFirst is passed in as false', async () => { 71 | const redisGetSpy = jest.spyOn(redis, 'getProtocolUserDataRedis'); 72 | const redisSetSpy = jest.spyOn(redis, 'setProtocolUserDataRedis'); 73 | 74 | const rpcSpy = jest.spyOn(rpc, 'getProtocolUserDataRPC'); 75 | 76 | const result = await getProtocolUserData(poolAddress, userAddress, false); 77 | expect(result).toEqual(getProtocolUserDataRPCMockResponse); 78 | 79 | expect(redisGetSpy).toHaveBeenCalledTimes(0); 80 | 81 | expect(rpcSpy).toHaveBeenCalledTimes(1); 82 | expect(rpcSpy).toHaveBeenCalledWith(poolAddress, userAddress); 83 | 84 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 85 | expect(redisSetSpy).toHaveBeenCalledWith( 86 | poolAddress, 87 | userAddress, 88 | getProtocolUserDataRPCMockResponse 89 | ); 90 | }); 91 | 92 | it('should call cache if cacheFirst is passed in as true (empty cache path)', async () => { 93 | const redisGetSpy = jest.spyOn(redis, 'getProtocolUserDataRedis'); 94 | const redisSetSpy = jest.spyOn(redis, 'setProtocolUserDataRedis'); 95 | 96 | const rpcSpy = jest.spyOn(rpc, 'getProtocolUserDataRPC'); 97 | 98 | const result = await getProtocolUserData(poolAddress, userAddress, true); 99 | expect(result).toEqual(getProtocolUserDataRPCMockResponse); 100 | 101 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 102 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress, userAddress); 103 | 104 | expect(rpcSpy).toHaveBeenCalledTimes(1); 105 | expect(rpcSpy).toHaveBeenCalledWith(poolAddress, userAddress); 106 | 107 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 108 | expect(redisSetSpy).toHaveBeenCalledWith( 109 | poolAddress, 110 | userAddress, 111 | getProtocolUserDataRPCMockResponse 112 | ); 113 | }); 114 | 115 | it('should call cache if cacheFirst is passed in as true (cache defined path)', async () => { 116 | const redisGetSpy = jest 117 | .spyOn(redis, 'getProtocolUserDataRedis') 118 | .mockImplementationOnce(async () => getProtocolUserDataRPCMockResponse as any); 119 | const redisSetSpy = jest.spyOn(redis, 'setProtocolUserDataRedis'); 120 | 121 | const rpcSpy = jest.spyOn(rpc, 'getProtocolUserDataRPC'); 122 | 123 | const result = await getProtocolUserData(poolAddress, userAddress, true); 124 | expect(result).toEqual(getProtocolUserDataRPCMockResponse); 125 | 126 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 127 | expect(redisGetSpy).toHaveBeenCalledWith(poolAddress, userAddress); 128 | 129 | expect(rpcSpy).toHaveBeenCalledTimes(0); 130 | 131 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /backend/__tests__/services/pool-data/rpc.spec.ts: -------------------------------------------------------------------------------- 1 | import { getProtocolDataRPC, getProtocolUserDataRPC } from '../../../src/services/pool-data/rpc'; 2 | import { poolAddress, userAddress } from '../../mocks'; 3 | 4 | xdescribe('rpc', () => { 5 | describe('getProtocolDataRPC', () => { 6 | it('should return `ProtocolData`', async () => { 7 | const result = await getProtocolDataRPC(poolAddress); 8 | expect(result).toBeInstanceOf(Object); 9 | expect(result.reserves).toBeInstanceOf(Object); 10 | expect(result.baseCurrencyData).toBeInstanceOf(Object); 11 | }); 12 | }); 13 | 14 | describe('getProtocolUserDataRPC', () => { 15 | it('should return `UserData`', async () => { 16 | const result = await getProtocolUserDataRPC(poolAddress, userAddress); 17 | expect(result).toBeInstanceOf(Object); 18 | expect(result).toBeInstanceOf(Array); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /backend/__tests__/services/stake-data/provier.spec.ts: -------------------------------------------------------------------------------- 1 | import * as redis from '../../../src/redis'; 2 | import { 3 | getStakeGeneralUIData, 4 | getStakeUserUIData, 5 | } from '../../../src/services/stake-data/provider'; 6 | import * as rpc from '../../../src/services/stake-data/rpc'; 7 | import { userAddress } from '../../mocks'; 8 | 9 | jest.mock('../../../src/redis', () => ({ 10 | __esModule: true, 11 | getStakeUserUIDataRedis: jest.fn(), 12 | setStakeUserUIDataRedis: jest.fn(), 13 | getStakeGeneralUIDataRedis: jest.fn(), 14 | setStakeGeneralUIDataRedis: jest.fn(), 15 | })); 16 | 17 | const getStakeUserUIDataRPCMockResponse = 'mockedGetStakeUserUIDataRPC'; 18 | const getStakeGeneralUIDataRPCMockResponse = 'mockedGetStakeGeneralUIDataRPC'; 19 | 20 | jest.mock('../../../src/services/stake-data/rpc', () => ({ 21 | __esModule: true, 22 | getUserStakeUIDataRPC: jest.fn().mockImplementation(() => getStakeUserUIDataRPCMockResponse), 23 | getGeneralStakeUIDataRPC: jest 24 | .fn() 25 | .mockImplementation(() => getStakeGeneralUIDataRPCMockResponse), 26 | })); 27 | 28 | describe('provider', () => { 29 | describe('getStakeUserUIData', () => { 30 | it('should not call cache if forceCache is passed in as true', async () => { 31 | const redisGetSpy = jest.spyOn(redis, 'getStakeUserUIDataRedis'); 32 | const redisSetSpy = jest.spyOn(redis, 'setStakeUserUIDataRedis'); 33 | 34 | const rpcSpy = jest.spyOn(rpc, 'getUserStakeUIDataRPC'); 35 | 36 | const result = await getStakeUserUIData(userAddress, true); 37 | expect(result).toEqual(getStakeUserUIDataRPCMockResponse); 38 | 39 | expect(redisGetSpy).toHaveBeenCalledTimes(0); 40 | 41 | expect(rpcSpy).toHaveBeenCalledTimes(1); 42 | expect(rpcSpy).toHaveBeenCalledWith(userAddress); 43 | 44 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 45 | expect(redisSetSpy).toHaveBeenCalledWith(userAddress, getStakeUserUIDataRPCMockResponse); 46 | }); 47 | 48 | it('should call cache if forceCache is passed in as false (empty cache path)', async () => { 49 | const redisGetSpy = jest.spyOn(redis, 'getStakeUserUIDataRedis'); 50 | const redisSetSpy = jest.spyOn(redis, 'setStakeUserUIDataRedis'); 51 | 52 | const rpcSpy = jest.spyOn(rpc, 'getUserStakeUIDataRPC'); 53 | 54 | const result = await getStakeUserUIData(userAddress, false); 55 | expect(result).toEqual(getStakeUserUIDataRPCMockResponse); 56 | 57 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 58 | expect(redisGetSpy).toHaveBeenCalledWith(userAddress); 59 | 60 | expect(rpcSpy).toHaveBeenCalledTimes(1); 61 | expect(rpcSpy).toHaveBeenCalledWith(userAddress); 62 | 63 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 64 | expect(redisSetSpy).toHaveBeenCalledWith(userAddress, getStakeUserUIDataRPCMockResponse); 65 | }); 66 | 67 | it('should call cache if forceCache is passed in as false (cache defined path)', async () => { 68 | const redisGetSpy = jest 69 | .spyOn(redis, 'getStakeUserUIDataRedis') 70 | .mockImplementationOnce(async () => getStakeUserUIDataRPCMockResponse as any); 71 | const redisSetSpy = jest.spyOn(redis, 'setStakeUserUIDataRedis'); 72 | 73 | const rpcSpy = jest.spyOn(rpc, 'getUserStakeUIDataRPC'); 74 | 75 | const result = await getStakeUserUIData(userAddress, false); 76 | expect(result).toEqual(getStakeUserUIDataRPCMockResponse); 77 | 78 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 79 | expect(redisGetSpy).toHaveBeenCalledWith(userAddress); 80 | 81 | expect(rpcSpy).toHaveBeenCalledTimes(0); 82 | 83 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 84 | }); 85 | }); 86 | 87 | describe('getStakeGeneralUIData', () => { 88 | it('should not call cache if forceCache is passed in as true', async () => { 89 | const redisGetSpy = jest.spyOn(redis, 'getStakeGeneralUIDataRedis'); 90 | const redisSetSpy = jest.spyOn(redis, 'setStakeGeneralUIDataRedis'); 91 | 92 | const rpcSpy = jest.spyOn(rpc, 'getGeneralStakeUIDataRPC'); 93 | 94 | const result = await getStakeGeneralUIData(true); 95 | expect(result).toEqual(getStakeGeneralUIDataRPCMockResponse); 96 | 97 | expect(redisGetSpy).toHaveBeenCalledTimes(0); 98 | 99 | expect(rpcSpy).toHaveBeenCalledTimes(1); 100 | expect(rpcSpy).toHaveBeenCalledWith(); 101 | 102 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 103 | expect(redisSetSpy).toHaveBeenCalledWith(getStakeGeneralUIDataRPCMockResponse); 104 | }); 105 | 106 | it('should call cache if forceCache is passed in as false (empty cache path)', async () => { 107 | const redisGetSpy = jest.spyOn(redis, 'getStakeGeneralUIDataRedis'); 108 | const redisSetSpy = jest.spyOn(redis, 'setStakeGeneralUIDataRedis'); 109 | 110 | const rpcSpy = jest.spyOn(rpc, 'getGeneralStakeUIDataRPC'); 111 | 112 | const result = await getStakeGeneralUIData(false); 113 | expect(result).toEqual(getStakeGeneralUIDataRPCMockResponse); 114 | 115 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 116 | expect(redisGetSpy).toHaveBeenCalledWith(); 117 | 118 | expect(rpcSpy).toHaveBeenCalledTimes(1); 119 | expect(rpcSpy).toHaveBeenCalledWith(); 120 | 121 | expect(redisSetSpy).toHaveBeenCalledTimes(1); 122 | expect(redisSetSpy).toHaveBeenCalledWith(getStakeGeneralUIDataRPCMockResponse); 123 | }); 124 | 125 | it('should call cache if forceCache is passed in as false (cache defined path)', async () => { 126 | const redisGetSpy = jest 127 | .spyOn(redis, 'getStakeGeneralUIDataRedis') 128 | .mockImplementationOnce(async () => getStakeGeneralUIDataRPCMockResponse as any); 129 | const redisSetSpy = jest.spyOn(redis, 'setStakeGeneralUIDataRedis'); 130 | 131 | const rpcSpy = jest.spyOn(rpc, 'getGeneralStakeUIDataRPC'); 132 | 133 | const result = await getStakeGeneralUIData(false); 134 | expect(result).toEqual(getStakeGeneralUIDataRPCMockResponse); 135 | 136 | expect(redisGetSpy).toHaveBeenCalledTimes(1); 137 | expect(redisGetSpy).toHaveBeenCalledWith(); 138 | 139 | expect(rpcSpy).toHaveBeenCalledTimes(0); 140 | 141 | expect(redisSetSpy).toHaveBeenCalledTimes(0); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /backend/__tests__/services/stake-data/rpc.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getGeneralStakeUIDataRPC, 3 | getUserStakeUIDataRPC, 4 | } from '../../../src/services/stake-data/rpc'; 5 | import { poolAddress } from '../../mocks'; 6 | 7 | describe('rpc', () => { 8 | describe('getUserStakeUIDataRPC', () => { 9 | it('should return `StakeUserUIData`', async () => { 10 | const result = await getUserStakeUIDataRPC(poolAddress); 11 | expect(result).toBeInstanceOf(Object); 12 | expect(result.aave).toBeInstanceOf(Object); 13 | expect(result.aave.stakeTokenUserBalance).not.toBeUndefined(); 14 | // @ts-ignore 15 | expect(result.aave.stakeTokenTotalSupply).toBeUndefined(); 16 | expect(result.bpt).toBeInstanceOf(Object); 17 | expect(result.bpt.stakeTokenUserBalance).not.toBeUndefined(); 18 | // @ts-ignore 19 | expect(result.bpt.stakeTokenTotalSupply).toBeUndefined(); 20 | expect(typeof result.usdPriceEth).toEqual('string'); 21 | }); 22 | }); 23 | 24 | describe('getGeneralStakeUIDataRPC', () => { 25 | it('should return `StakeGeneralUIData`', async () => { 26 | const result = await getGeneralStakeUIDataRPC(); 27 | expect(result).toBeInstanceOf(Object); 28 | expect(result.aave).toBeInstanceOf(Object); 29 | expect(result.aave.stakeTokenTotalSupply).not.toBeUndefined(); 30 | // @ts-ignore 31 | expect(result.aave.stakeTokenUserBalance).toBeUndefined(); 32 | expect(result.bpt).toBeInstanceOf(Object); 33 | expect(result.bpt.stakeTokenTotalSupply).not.toBeUndefined(); 34 | // @ts-ignore 35 | expect(result.bpt.stakeTokenUserBalance).toBeUndefined(); 36 | expect(typeof result.usdPriceEth).toEqual('string'); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /backend/__tests__/setup-jest.ts: -------------------------------------------------------------------------------- 1 | console.log = jest.fn(); 2 | console.error = jest.fn(); 3 | 4 | jest.mock('../src/redis/shared', () => ({ 5 | __esModule: true, 6 | getRedis: jest.fn(), 7 | })); 8 | 9 | jest.mock('../src/pubsub', () => ({ 10 | __esModule: true, 11 | getPubSub: jest.fn(), 12 | pushUpdatedReserveDataToSubscriptions: jest.fn(), 13 | pushUpdatedUserReserveDataToSubscriptions: jest.fn(), 14 | pushUpdatedStakeUserDataToSubscriptions: jest.fn(), 15 | pushUpdatedPoolIncentivesDataToSubscriptions: jest.fn(), 16 | pushUpdatedUserPoolIncentivesDataToSubscriptions: jest.fn(), 17 | })); 18 | 19 | jest.mock('../src/config', () => ({ 20 | __esModule: true, 21 | REDIS_HOST: 'redis', 22 | NETWORK: 'mainnet', 23 | PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES: [ 24 | '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5', 25 | '0xacc030ef66f9dfeae9cbb0cd1b25654b82cfa8d5', 26 | ], 27 | POOL_UI_DATA_PROVIDER_ADDRESS: '0x47e300dDd1d25447482E2F7e5a5a967EA2DA8634', 28 | AAVE_TOKEN_ADDRESS: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', 29 | ABPT_TOKEN: '0x41A08648C3766F9F9d85598fF102a08f4ef84F84', 30 | STK_AAVE_TOKEN_ADDRESS: '0x4da27a545c0c5B758a6BA100e3a049001de870f5', 31 | STK_ABPT_TOKEN_ADDRESS: '0xa1116930326D21fB917d5A27F1E9943A9595fb47', 32 | STAKE_DATA_PROVIDER: '0xc57450af527d10Fe182521AB39C1AD23c1e1BaDE', 33 | UI_INCENTIVE_DATA_PROVIDER_ADDRESS: '0xd9F1e5F70B14b8Fd577Df84be7D75afB8a3A0186', 34 | STAKE_DATA_POOLING_INTERVAL: 1000, 35 | BLOCK_NUMBER_POOLING_INTERVAL: 1000, 36 | RPC_URL: 'https://eth-mainnet.alchemyapi.io/v2/demo', 37 | RPC_MAX_TIMEOUT: 1000, 38 | BACKUP_RPC_URLS: [], 39 | USERS_DATA_POOLING_INTERVAL: 5000, 40 | GENERAL_RESERVES_DATA_POOLING_INTERVAL: 5000, 41 | RECOVERY_TIMEOUT: 5000, 42 | RESERVES_LIST_VALIDITY_INTERVAL: 60 * 5 * 1000, 43 | })); 44 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/last-seen-block.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { add, get, remove, update } from '../../src/tasks/last-seen-block.state'; 2 | 3 | describe('lastSeenBlockState', () => { 4 | const key = 'KEY'; 5 | const key2 = 'KEY2'; 6 | 7 | it('should add key', () => { 8 | remove(key); 9 | remove(key2); 10 | add(key, 1); 11 | add(key2, 99); 12 | expect(get(key)).toEqual(1); 13 | expect(get(key2)).toEqual(99); 14 | }); 15 | 16 | it('should update key', () => { 17 | remove(key); 18 | remove(key2); 19 | add(key, 1); 20 | add(key2, 99); 21 | update(key, 2); 22 | expect(get(key)).toEqual(2); 23 | expect(get(key2)).toEqual(99); 24 | }); 25 | 26 | it('should upsert key if it doesnt exist', () => { 27 | remove(key); 28 | remove(key2); 29 | add(key2, 99); 30 | 31 | update(key, 2); 32 | expect(get(key)).toEqual(2); 33 | expect(get(key2)).toEqual(99); 34 | }); 35 | 36 | it('should remove key', () => { 37 | remove(key); 38 | remove(key2); 39 | add(key, 1); 40 | add(key2, 99); 41 | expect(get(key)).toEqual(1); 42 | remove(key); 43 | expect(get(key)).toEqual(0); 44 | expect(get(key2)).toEqual(99); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/task-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ethereum from '../../src/helpers/ethereum'; 2 | import { sleep } from '../../src/helpers/utils'; 3 | import * as lastSeenBlockState from '../../src/tasks/last-seen-block.state'; 4 | import { getBlockContext, runTask } from '../../src/tasks/task-helpers'; 5 | 6 | jest.mock('../../src/helpers/ethereum', () => ({ 7 | __esModule: true, 8 | getBlockNumber: jest.fn(), 9 | })); 10 | 11 | jest.mock('../../src/redis', () => ({ 12 | __esModule: true, 13 | setBlockNumberRedis: jest.fn(), 14 | })); 15 | 16 | describe('taskHelpers', () => { 17 | describe('runTask', () => { 18 | it('should hit the handler the correct amount of times and once running is stopped not hit it again', async () => { 19 | let count = 0; 20 | let running = true; 21 | 22 | runTask({ 23 | runEvery: 100, 24 | mainHandler: () => { 25 | count++; 26 | }, 27 | runningHandler: () => running, 28 | }); 29 | 30 | await sleep(150); 31 | expect(count).toEqual(2); 32 | running = false; 33 | await sleep(100); 34 | expect(count).toEqual(2); 35 | }); 36 | 37 | it('should not fire the handler until the startup has loaded', async () => { 38 | let count = 0; 39 | let running = true; 40 | runTask({ 41 | runEvery: 100, 42 | startupHandler: async () => { 43 | await sleep(200); 44 | }, 45 | mainHandler: () => { 46 | count++; 47 | }, 48 | runningHandler: () => running, 49 | }); 50 | 51 | await sleep(150); 52 | expect(count).toEqual(0); 53 | await sleep(200); 54 | expect(count).toBeGreaterThanOrEqual(1); 55 | running = false; 56 | }); 57 | }); 58 | 59 | describe('getBlockContext', () => { 60 | it('should call `getBlockNumber` with cache as true', async () => { 61 | const spy = jest.spyOn(ethereum, 'getBlockNumber'); 62 | 63 | await getBlockContext(''); 64 | 65 | expect(spy).toHaveBeenCalledTimes(1); 66 | expect(spy).toHaveBeenCalledWith(true); 67 | }); 68 | 69 | it('should call `getBlockNumber` with cache as false', async () => { 70 | const spy = jest.spyOn(ethereum, 'getBlockNumber'); 71 | 72 | await getBlockContext('', false); 73 | 74 | expect(spy).toHaveBeenCalledTimes(1); 75 | expect(spy).toHaveBeenCalledWith(false); 76 | }); 77 | 78 | it('should call `lastSeenBlockState.get` with key', async () => { 79 | const spy = jest.spyOn(lastSeenBlockState, 'get'); 80 | 81 | await getBlockContext('key', false); 82 | 83 | expect(spy).toHaveBeenCalledTimes(1); 84 | expect(spy).toHaveBeenCalledWith('key'); 85 | }); 86 | 87 | it('should call return correct response', async () => { 88 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(() => Promise.resolve(-1)); 89 | 90 | const result = await getBlockContext('', false); 91 | 92 | expect(result.currentBlock).toEqual(-1); 93 | expect(result.lastSeenBlock).toEqual(0); 94 | expect(result.shouldExecute).toEqual(false); 95 | expect(result.commit).not.toBeUndefined(); 96 | }); 97 | 98 | it('should call return correct response', async () => { 99 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(() => Promise.resolve(1)); 100 | 101 | const result = await getBlockContext('', false); 102 | 103 | expect(result.currentBlock).toEqual(1); 104 | expect(result.lastSeenBlock).toEqual(0); 105 | expect(result.shouldExecute).toEqual(true); 106 | expect(result.commit).not.toBeUndefined(); 107 | }); 108 | 109 | it('should call update ', async () => { 110 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(() => Promise.resolve(1)); 111 | 112 | const result = await getBlockContext('', false); 113 | 114 | expect(result.currentBlock).toEqual(1); 115 | expect(result.lastSeenBlock).toEqual(0); 116 | expect(result.shouldExecute).toEqual(true); 117 | expect(result.commit).not.toBeUndefined(); 118 | }); 119 | 120 | it('should call `lastSeenBlockState.update` with key and current block if commit is called', async () => { 121 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(() => Promise.resolve(1)); 122 | const spy = jest.spyOn(lastSeenBlockState, 'update'); 123 | 124 | const result = await getBlockContext('key', false); 125 | 126 | result.commit(); 127 | 128 | expect(spy).toHaveBeenCalledTimes(1); 129 | expect(spy).toHaveBeenCalledWith('key', result.currentBlock); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-block-number/handler.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ethereum from '../../../src/helpers/ethereum'; 2 | import * as redis from '../../../src/redis'; 3 | import { 4 | handler, 5 | running, 6 | startUp, 7 | stopHandler, 8 | } from '../../../src/tasks/update-block-number/handler'; 9 | 10 | jest.mock('../../../src/helpers/ethereum', () => ({ 11 | __esModule: true, 12 | getBlockNumber: jest.fn(), 13 | })); 14 | 15 | jest.mock('../../../src/redis', () => ({ 16 | __esModule: true, 17 | setBlockNumberRedis: jest.fn(), 18 | })); 19 | 20 | describe('update-block-number handler', () => { 21 | describe('handler', () => { 22 | it('should get the rpc block number and set it', async () => { 23 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 1); 24 | const spy2 = jest.spyOn(redis, 'setBlockNumberRedis'); 25 | await handler(); 26 | expect(spy).toHaveBeenCalledTimes(1); 27 | expect(spy2).toHaveBeenCalledTimes(1); 28 | }); 29 | 30 | it('should get the rpc block number and not set if last seen is lower then block number', async () => { 31 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 1); 32 | const spy2 = jest.spyOn(redis, 'setBlockNumberRedis'); 33 | await handler(); 34 | expect(spy).toHaveBeenCalledTimes(1); 35 | expect(spy2).toHaveBeenCalledTimes(0); 36 | }); 37 | }); 38 | 39 | describe('running + stopped + startup', () => { 40 | it('should return running false if stop handler is called', () => { 41 | startUp(); 42 | expect(running()).toEqual(true); 43 | stopHandler(); 44 | expect(running()).toEqual(false); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-general-reserves-data/handler.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ethereum from '../../../src/helpers/ethereum'; 2 | import * as pubsub from '../../../src/pubsub'; 3 | import * as redis from '../../../src/redis'; 4 | import * as poolDataRpc from '../../../src/services/pool-data/rpc'; 5 | import { 6 | handler, 7 | running, 8 | startUp, 9 | stopHandler, 10 | } from '../../../src/tasks/update-general-reserves-data/handler'; 11 | import { poolAddress } from '../../mocks'; 12 | 13 | jest.mock('../../../src/helpers/ethereum', () => ({ 14 | __esModule: true, 15 | getBlockNumber: jest.fn(), 16 | })); 17 | 18 | jest.mock('../../../src/redis', () => ({ 19 | __esModule: true, 20 | setBlockNumberRedis: jest.fn(), 21 | getProtocolDataRedis: jest.fn(), 22 | setProtocolDataRedis: jest.fn(), 23 | })); 24 | 25 | const getProtocolDataRedisMock = { 26 | data: 'mockedGetProtocolDataRPC', 27 | hash: 'vCQEpPWzvsdKk6A6A9jFfg==', 28 | }; 29 | const getProtocolDataRPCMockResponse = 'mockedGetProtocolDataRPC'; 30 | 31 | jest.mock('../../../src/services/pool-data/rpc', () => ({ 32 | __esModule: true, 33 | getProtocolDataRPC: jest.fn().mockImplementation(() => getProtocolDataRPCMockResponse), 34 | })); 35 | 36 | describe('update-general-reserves-data', () => { 37 | describe('handler', () => { 38 | it('should get the block number once', async () => { 39 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 0); 40 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 1); 41 | await handler(poolAddress); 42 | expect(spy).toHaveBeenCalledTimes(1); 43 | }); 44 | 45 | it('should only hit `getBlockNumber` if returns undefined', async () => { 46 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => undefined); 47 | await handler(poolAddress); 48 | expect(spy).toHaveBeenCalledTimes(1); 49 | }); 50 | 51 | it('should call `getProtocolDataRPC` once', async () => { 52 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 2); 53 | const spy = jest.spyOn(poolDataRpc, 'getProtocolDataRPC'); 54 | await handler(poolAddress); 55 | expect(spy).toHaveBeenCalledTimes(1); 56 | expect(spy).toHaveBeenCalledWith(poolAddress); 57 | }); 58 | 59 | it('should call `getProtocolDataRedis` once', async () => { 60 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 3); 61 | const spy = jest.spyOn(redis, 'getProtocolDataRedis'); 62 | await handler(poolAddress); 63 | expect(spy).toHaveBeenCalledTimes(1); 64 | expect(spy).toHaveBeenCalledWith(poolAddress); 65 | }); 66 | 67 | it('should call `pushUpdatedReserveDataToSubscriptions` with the new data', async () => { 68 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 4); 69 | const spy = jest.spyOn(pubsub, 'pushUpdatedReserveDataToSubscriptions'); 70 | await handler(poolAddress); 71 | expect(spy).toHaveBeenCalledTimes(1); 72 | expect(spy).toHaveBeenCalledWith(poolAddress, getProtocolDataRPCMockResponse); 73 | }); 74 | 75 | it('should call `pushUpdatedReserveDataToSubscriptions` with the new data', async () => { 76 | jest.spyOn(poolDataRpc, 'getProtocolDataRPC').mockImplementation(async () => { 77 | return { hey: true } as any; 78 | }); 79 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 5); 80 | 81 | const spy = jest.spyOn(pubsub, 'pushUpdatedReserveDataToSubscriptions'); 82 | await handler(poolAddress); 83 | expect(spy).toHaveBeenCalledTimes(1); 84 | expect(spy).toHaveBeenCalledWith(poolAddress, { hey: true }); 85 | }); 86 | 87 | it('should not call `pushUpdatedReserveDataToSubscriptions` with data that is the same', async () => { 88 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 6); 89 | jest 90 | .spyOn(poolDataRpc, 'getProtocolDataRPC') 91 | .mockImplementation(async () => getProtocolDataRPCMockResponse as any); 92 | jest.spyOn(redis, 'getProtocolDataRedis').mockImplementation(async () => { 93 | return getProtocolDataRedisMock as any; 94 | }); 95 | const spy = jest.spyOn(pubsub, 'pushUpdatedReserveDataToSubscriptions'); 96 | 97 | await handler(poolAddress); 98 | expect(spy).toHaveBeenCalledTimes(0); 99 | 100 | jest.spyOn(redis, 'getProtocolDataRedis').mockImplementation(async () => undefined); 101 | }); 102 | 103 | it('should call `setProtocolDataRedis` with the new data', async () => { 104 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 8); 105 | const spy = jest.spyOn(redis, 'setProtocolDataRedis'); 106 | await handler(poolAddress); 107 | expect(spy).toHaveBeenCalledTimes(1); 108 | expect(spy).toHaveBeenCalledWith(poolAddress, getProtocolDataRedisMock); 109 | }); 110 | 111 | it('should call `setProtocolDataRedis` with the new data', async () => { 112 | jest.spyOn(poolDataRpc, 'getProtocolDataRPC').mockImplementation(async () => { 113 | return { hey: true } as any; 114 | }); 115 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 9); 116 | const spy = jest.spyOn(redis, 'setProtocolDataRedis'); 117 | await handler(poolAddress); 118 | expect(spy).toHaveBeenCalledTimes(1); 119 | expect(spy).toHaveBeenCalledWith(poolAddress, { 120 | data: { hey: true }, 121 | hash: 'Hy2WrvTaiFkSXA0olfqraQ==', 122 | }); 123 | }); 124 | 125 | it('should not call `setProtocolDataRedis` with data that is the same', async () => { 126 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 10); 127 | jest 128 | .spyOn(poolDataRpc, 'getProtocolDataRPC') 129 | .mockImplementation(async () => getProtocolDataRPCMockResponse as any); 130 | jest.spyOn(redis, 'getProtocolDataRedis').mockImplementation(async () => { 131 | return getProtocolDataRedisMock as any; 132 | }); 133 | const spy = jest.spyOn(redis, 'setProtocolDataRedis'); 134 | await handler(poolAddress); 135 | expect(spy).toHaveBeenCalledTimes(0); 136 | }); 137 | 138 | it('should return if it sees the same block and not hit any methods', async () => { 139 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 10); 140 | stopHandler(poolAddress); 141 | await startUp(poolAddress); 142 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 9); 143 | const spy = jest.spyOn(poolDataRpc, 'getProtocolDataRPC'); 144 | await handler(poolAddress); 145 | expect(spy).toHaveBeenCalledTimes(0); 146 | }); 147 | }); 148 | 149 | describe('running + stopped + startup', () => { 150 | it('should return running false if stop handler is called', async () => { 151 | await startUp(poolAddress); 152 | expect(running()).toEqual(true); 153 | stopHandler(poolAddress); 154 | expect(running()).toEqual(false); 155 | }); 156 | 157 | it('should call getBlockNumber once', async () => { 158 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 20); 159 | await startUp(poolAddress); 160 | expect(spy).toHaveBeenCalledTimes(1); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-stake-general-ui-data/handler.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ethereum from '../../../src/helpers/ethereum'; 2 | import * as pubsub from '../../../src/pubsub'; 3 | import * as redis from '../../../src/redis'; 4 | import * as RPC from '../../../src/services/stake-data/rpc'; 5 | import { 6 | handler, 7 | running, 8 | startUp, 9 | stopHandler, 10 | } from '../../../src/tasks/update-stake-general-ui-data/handler'; 11 | 12 | jest.mock('../../../src/redis', () => ({ 13 | __esModule: true, 14 | setStakeGeneralUIDataRedis: jest.fn(), 15 | })); 16 | 17 | jest.mock('../../../src/helpers/ethereum', () => ({ 18 | __esModule: true, 19 | getBlockNumber: jest.fn(), 20 | })); 21 | 22 | jest.mock('../../../src/services/stake-data/rpc', () => ({ 23 | __esModule: true, 24 | getGeneralStakeUIDataRPC: jest.fn(), 25 | })); 26 | 27 | jest.mock('../../../src/pubsub', () => ({ 28 | __esModule: true, 29 | pushUpdatedStakeGeneralUIDataToSubscriptions: jest.fn(), 30 | })); 31 | 32 | describe('stake-general-ui-data', () => { 33 | describe('handler', () => { 34 | it('should get the block number once', async () => { 35 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 1); 36 | await handler(); 37 | expect(spy).toHaveBeenCalledTimes(1); 38 | }); 39 | 40 | it('should call `getGeneralStakeUIDataRPC`', async () => { 41 | jest.spyOn(RPC, 'getGeneralStakeUIDataRPC').mockImplementation(async () => { 42 | return { hey: 2 } as any; 43 | }); 44 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 2); 45 | const spy = jest.spyOn(RPC, 'getGeneralStakeUIDataRPC'); 46 | await handler(); 47 | expect(spy).toHaveBeenCalledTimes(1); 48 | }); 49 | 50 | it('should call `setStakeGeneralUIDataRedis`', async () => { 51 | jest.spyOn(RPC, 'getGeneralStakeUIDataRPC').mockImplementation(async () => { 52 | return { hey: 3 } as any; 53 | }); 54 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 3); 55 | const spy = jest.spyOn(redis, 'setStakeGeneralUIDataRedis'); 56 | await handler(); 57 | expect(spy).toHaveBeenCalledTimes(1); 58 | }); 59 | 60 | it('should call `pushUpdatedStakeGeneralUIDataToSubscriptions` ', async () => { 61 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 4); 62 | jest.spyOn(RPC, 'getGeneralStakeUIDataRPC').mockImplementation(async () => { 63 | return { hey: 4 } as any; 64 | }); 65 | const spy = jest.spyOn(pubsub, 'pushUpdatedStakeGeneralUIDataToSubscriptions'); 66 | await handler(); 67 | expect(spy).toHaveBeenCalledTimes(1); 68 | }); 69 | 70 | it('should not call `pushUpdatedStakeGeneralUIDataToSubscriptions` or `getGeneralStakeUIDataRPC` or `setStakeGeneralUIDataRedis` if block number is not higher', async () => { 71 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 4); 72 | jest.spyOn(RPC, 'getGeneralStakeUIDataRPC').mockImplementation(async () => { 73 | return { hey: 5 } as any; 74 | }); 75 | const stakeSpy = jest.spyOn(RPC, 'getGeneralStakeUIDataRPC'); 76 | const pubSubSpy = jest.spyOn(pubsub, 'pushUpdatedStakeGeneralUIDataToSubscriptions'); 77 | const redisSpy = jest.spyOn(redis, 'setStakeGeneralUIDataRedis'); 78 | await handler(); 79 | expect(stakeSpy).toHaveBeenCalledTimes(0); 80 | expect(pubSubSpy).toHaveBeenCalledTimes(0); 81 | expect(redisSpy).toHaveBeenCalledTimes(0); 82 | }); 83 | 84 | it('should not call `pushUpdatedStakeGeneralUIDataToSubscriptions` or `setStakeGeneralUIDataRedis` if last seen stored data hash is the same', async () => { 85 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 5); 86 | jest.spyOn(RPC, 'getGeneralStakeUIDataRPC').mockImplementation(async () => { 87 | return { hey: 4 } as any; 88 | }); 89 | const stakeSpy = jest.spyOn(RPC, 'getGeneralStakeUIDataRPC'); 90 | const pubSubSpy = jest.spyOn(pubsub, 'pushUpdatedStakeGeneralUIDataToSubscriptions'); 91 | const redisSpy = jest.spyOn(redis, 'setStakeGeneralUIDataRedis'); 92 | await handler(); 93 | expect(stakeSpy).toHaveBeenCalledTimes(1); 94 | expect(pubSubSpy).toHaveBeenCalledTimes(0); 95 | expect(redisSpy).toHaveBeenCalledTimes(0); 96 | }); 97 | }); 98 | 99 | describe('running + stopped + startup', () => { 100 | it('should return running false if stop handler is called', () => { 101 | startUp(); 102 | expect(running()).toEqual(true); 103 | stopHandler(); 104 | expect(running()).toEqual(false); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-stake-general-ui-data/last-stored-hash.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { get, set } from '../../../src/tasks/update-stake-general-ui-data/last-stored-hash.state'; 2 | 3 | describe('lastSeenStoredDataHash', () => { 4 | it('should be default empty string', () => { 5 | expect(get()).toEqual(''); 6 | }); 7 | 8 | it('should set and get correctly', () => { 9 | expect(get()).toEqual(''); 10 | set('123'); 11 | expect(get()).toEqual('123'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-stake-user-ui-data/handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AAVE_TOKEN_ADDRESS, 3 | ABPT_TOKEN, 4 | STK_AAVE_TOKEN_ADDRESS, 5 | STK_ABPT_TOKEN_ADDRESS, 6 | } from '../../../src/config'; 7 | import * as ethereum from '../../../src/helpers/ethereum'; 8 | import * as pubsub from '../../../src/pubsub'; 9 | import { 10 | handler, 11 | running, 12 | startUp, 13 | stopHandler, 14 | } from '../../../src/tasks/update-stake-user-ui-data/handler'; 15 | import { userAddress } from '../../mocks'; 16 | 17 | jest.mock('../../../src/helpers/ethereum', () => ({ 18 | __esModule: true, 19 | getBlockNumber: jest.fn(), 20 | getUsersFromLogs: jest.fn().mockImplementation(() => []), 21 | })); 22 | 23 | jest.mock('../../../src/pubsub', () => ({ 24 | __esModule: true, 25 | pushUpdatedStakeUserUIDataToSubscriptions: jest.fn(), 26 | })); 27 | 28 | describe('update-stake-user-ui-data', () => { 29 | describe('handler', () => { 30 | it('should get the block number once', async () => { 31 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 1); 32 | await handler(); 33 | expect(spy).toHaveBeenCalledTimes(1); 34 | }); 35 | 36 | it('should call `getUsersFromLogs` twice', async () => { 37 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 2); 38 | const spy = jest.spyOn(ethereum, 'getUsersFromLogs'); 39 | await handler(); 40 | expect(spy).toHaveBeenCalledTimes(2); 41 | expect(spy.mock.calls).toEqual([ 42 | [ 43 | [STK_AAVE_TOKEN_ADDRESS, STK_ABPT_TOKEN_ADDRESS], 44 | 1, 45 | 2, 46 | ['RewardsClaimed(address,address,uint256)'], 47 | ], 48 | [ 49 | [STK_AAVE_TOKEN_ADDRESS, STK_ABPT_TOKEN_ADDRESS, AAVE_TOKEN_ADDRESS, ABPT_TOKEN], 50 | 1, 51 | 2, 52 | ['Transfer(address,address,uint256)'], 53 | ], 54 | ]); 55 | }); 56 | 57 | it('should not call `pushUpdatedStakeUserUIDataToSubscriptions` if no users', async () => { 58 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 3); 59 | const spy = jest.spyOn(pubsub, 'pushUpdatedStakeUserUIDataToSubscriptions'); 60 | await handler(); 61 | expect(spy).toHaveBeenCalledTimes(0); 62 | }); 63 | 64 | it('should call `pushUpdatedStakeUserUIDataToSubscriptions` as many times as users returned, this also tests that it removes duplicates', async () => { 65 | jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 4); 66 | jest.spyOn(ethereum, 'getUsersFromLogs').mockImplementation(async () => [userAddress]); 67 | const spy = jest.spyOn(pubsub, 'pushUpdatedStakeUserUIDataToSubscriptions'); 68 | await handler(); 69 | expect(spy).toHaveBeenCalledTimes(1); 70 | }); 71 | }); 72 | 73 | describe('running + stopped + startup', () => { 74 | it('should return running false if stop handler is called', async () => { 75 | await startUp(); 76 | expect(running()).toEqual(true); 77 | stopHandler(); 78 | expect(running()).toEqual(false); 79 | }); 80 | 81 | it('should call getBlockNumber once', async () => { 82 | const spy = jest.spyOn(ethereum, 'getBlockNumber').mockImplementation(async () => 20); 83 | await startUp(); 84 | expect(spy).toHaveBeenCalledTimes(1); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-users-data/pool-contracts.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { ILendingPoolAddressesProviderFactory } from '../../../src/contracts/ethers/ILendingPoolAddressesProviderFactory'; 2 | import { ILendingPoolFactory } from '../../../src/contracts/ethers/ILendingPoolFactory'; 3 | import { add, get, init } from '../../../src/tasks/update-users-data/pool-contracts.state'; 4 | import { poolAddress, userAddress } from '../../mocks'; 5 | 6 | const getIncentivesControllerAddressRpcMock = 'getIncentivesControllerAddressRpcMock'; 7 | 8 | jest.mock('../../../src/services/pool-data/rpc', () => ({ 9 | __esModule: true, 10 | getIncentivesControllerAddressRpc: jest 11 | .fn() 12 | .mockImplementation(() => getIncentivesControllerAddressRpcMock), 13 | })); 14 | 15 | describe('poolContractsState', () => { 16 | describe('get', () => { 17 | it('should throw error if nothing in pool contracts', () => { 18 | expect(() => { 19 | get(poolAddress); 20 | }).toThrow(); 21 | }); 22 | 23 | it('should return pool contracts if defined', () => { 24 | const _object = { 25 | lendingPoolAddressProvider: poolAddress, 26 | incentiveAddress: '123', 27 | lendingPoolContract: {} as any, 28 | }; 29 | add(_object); 30 | expect(get(poolAddress)).toEqual(_object); 31 | }); 32 | }); 33 | 34 | describe('add', () => { 35 | it('should add a pool contract', () => { 36 | const _object = { 37 | lendingPoolAddressProvider: poolAddress, 38 | incentiveAddress: '123', 39 | lendingPoolContract: {} as any, 40 | }; 41 | add(_object); 42 | expect(get(poolAddress)).toEqual(_object); 43 | }); 44 | 45 | it('should add a pool contracts', () => { 46 | const _object = { 47 | lendingPoolAddressProvider: poolAddress, 48 | incentiveAddress: '123', 49 | lendingPoolContract: {} as any, 50 | }; 51 | 52 | const _object2 = { 53 | lendingPoolAddressProvider: '123', 54 | incentiveAddress: '123', 55 | lendingPoolContract: {} as any, 56 | }; 57 | add(_object); 58 | add(_object2); 59 | expect(get(poolAddress)).toEqual(_object); 60 | expect(get('123')).toEqual(_object2); 61 | }); 62 | 63 | it('should add only 1 pool contracts', () => { 64 | const _object = { 65 | lendingPoolAddressProvider: poolAddress, 66 | incentiveAddress: '123', 67 | lendingPoolContract: {} as any, 68 | }; 69 | add(_object); 70 | add(_object); 71 | add(_object); 72 | expect(get(poolAddress)).toEqual(_object); 73 | }); 74 | }); 75 | 76 | describe('init', () => { 77 | const ethereumProviderMock = {} as any; 78 | const lendingPool = '0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9'; 79 | const reserveUsedAsCollateralDisabledMock = 'reserveUsedAsCollateralDisabledMock'; 80 | const reserveUsedAsCollateralEnabledMock = 'reserveUsedAsCollateralEnabledMock'; 81 | const ILendingPoolMock = { 82 | queryFilter: jest.fn().mockImplementation(() => [{ args: { user: userAddress } }]), 83 | filters: { 84 | ReserveUsedAsCollateralDisabled: jest 85 | .fn() 86 | .mockImplementation(() => reserveUsedAsCollateralDisabledMock), 87 | ReserveUsedAsCollateralEnabled: jest 88 | .fn() 89 | .mockImplementation(() => reserveUsedAsCollateralEnabledMock), 90 | }, 91 | }; 92 | 93 | beforeEach(() => { 94 | ILendingPoolAddressesProviderFactory.connect = jest.fn().mockImplementation(() => { 95 | return { 96 | getLendingPool: jest.fn().mockImplementation(() => lendingPool), 97 | }; 98 | }); 99 | 100 | ILendingPoolFactory.connect = jest.fn().mockImplementation(() => ILendingPoolMock); 101 | }); 102 | 103 | it('should connect to `ILendingPoolAddressesProviderFactory`', async () => { 104 | const spy = jest.spyOn(ILendingPoolAddressesProviderFactory, 'connect'); 105 | await init(poolAddress, ethereumProviderMock); 106 | expect(spy).toHaveBeenCalledTimes(1); 107 | expect(spy).toHaveBeenCalledWith(poolAddress, ethereumProviderMock); 108 | }); 109 | 110 | it('should connect to `ILendingPoolFactory`', async () => { 111 | const spy = jest.spyOn(ILendingPoolFactory, 'connect'); 112 | await init(poolAddress, ethereumProviderMock); 113 | expect(spy).toHaveBeenCalledTimes(1); 114 | expect(spy).toHaveBeenCalledWith(lendingPool, ethereumProviderMock); 115 | }); 116 | 117 | it('should add to pool contracts', async () => { 118 | await init(poolAddress, ethereumProviderMock); 119 | expect(get(poolAddress).lendingPoolAddressProvider).toEqual(poolAddress); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-users-data/protocol-data-reserves.state.spec.ts: -------------------------------------------------------------------------------- 1 | import * as poolDataProvider from '../../../src/services/pool-data/provider'; 2 | import { 3 | add, 4 | fetchAndAdd, 5 | get, 6 | } from '../../../src/tasks/update-users-data/protocol-data-reserves.state'; 7 | import { poolAddress } from '../../mocks'; 8 | 9 | const getProtocolDataMock = { 10 | reserves: [ 11 | { 12 | aTokenAddress: 'aTokenAddressMock1', 13 | stableDebtTokenAddress: 'stableDebtTokenAddressMock1', 14 | variableDebtTokenAddress: 'variableDebtTokenAddressMock1', 15 | }, 16 | { 17 | aTokenAddress: 'aTokenAddressMock2', 18 | stableDebtTokenAddress: 'stableDebtTokenAddressMock2', 19 | variableDebtTokenAddress: 'variableDebtTokenAddressMock2', 20 | }, 21 | ], 22 | }; 23 | 24 | jest.mock('../../../src/services/pool-data/provider', () => ({ 25 | __esModule: true, 26 | getProtocolData: jest.fn().mockImplementation(() => getProtocolDataMock), 27 | })); 28 | 29 | describe('protocolDataReservesState', () => { 30 | const poolAddress2 = '0x012'; 31 | 32 | describe('add + get', () => { 33 | it('should add a item and get it back', () => { 34 | add(poolAddress, ['1']); 35 | expect(get(poolAddress)).toEqual(['1']); 36 | }); 37 | 38 | it('should add only 1 item and get it back', () => { 39 | add(poolAddress, ['1']); 40 | add(poolAddress, ['2']); 41 | add(poolAddress, ['3']); 42 | add(poolAddress2, ['1']); 43 | expect(get(poolAddress)).toEqual(['3']); 44 | expect(get(poolAddress2)).toEqual(['1']); 45 | }); 46 | 47 | it('should throw if no reserves in list', () => { 48 | add(poolAddress, []); 49 | expect(() => { 50 | get(poolAddress); 51 | }).toThrowError(`reservesList for ${poolAddress} is empty`); 52 | }); 53 | 54 | it('should throw if no pool found', () => { 55 | expect(() => { 56 | get('UNKNOWN'); 57 | }).toThrowError('reservesList for UNKNOWN is empty'); 58 | }); 59 | }); 60 | 61 | describe('fetchAndAdd', () => { 62 | it('should call `getProtocolData`', async () => { 63 | const spy = jest.spyOn(poolDataProvider, 'getProtocolData'); 64 | await fetchAndAdd(poolAddress); 65 | expect(spy).toHaveBeenCalledTimes(1); 66 | expect(spy).toHaveBeenCalledWith(poolAddress); 67 | }); 68 | 69 | it('should add reserve lists for pool', async () => { 70 | const _poolAddress = 'POOOOOOOOOOL'; 71 | const list = await fetchAndAdd(_poolAddress); 72 | expect(get(_poolAddress)).toEqual(list); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-users-incentives-data/pool-contracts.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { ILendingPoolAddressesProviderFactory } from '../../../src/contracts/ethers/ILendingPoolAddressesProviderFactory'; 2 | import { ILendingPoolFactory } from '../../../src/contracts/ethers/ILendingPoolFactory'; 3 | import { 4 | add, 5 | get, 6 | init, 7 | } from '../../../src/tasks/update-users-incentives-data/pool-contracts.state'; 8 | import { poolAddress, userAddress } from '../../mocks'; 9 | import * as rpc from '../../../src/services/incentives-data'; 10 | 11 | const getPoolIncentivesRPCMockResponse = [ 12 | { 13 | aIncentiveData: { 14 | incentiveControllerAddress: 'aTokenAddressMock1', 15 | }, 16 | sIncentiveData: { 17 | incentiveControllerAddress: 'stableDebtTokenAddressMock1', 18 | }, 19 | vIncentiveData: { 20 | incentiveControllerAddress: 'variableDebtTokenAddressMock1', 21 | }, 22 | }, 23 | { 24 | aIncentiveData: { 25 | incentiveControllerAddress: 'aTokenAddressMock2', 26 | }, 27 | sIncentiveData: { 28 | incentiveControllerAddress: 'stableDebtTokenAddressMock2', 29 | }, 30 | vIncentiveData: { 31 | incentiveControllerAddress: 'variableDebtTokenAddressMock2', 32 | }, 33 | }, 34 | ]; 35 | jest.mock('../../../src/services/incentives-data', () => ({ 36 | __esModule: true, 37 | getPoolIncentivesRPC: jest.fn().mockImplementation(() => getPoolIncentivesRPCMockResponse), 38 | })); 39 | 40 | describe('poolContractsState', () => { 41 | describe('get', () => { 42 | it('should throw error if nothing in pool contracts', () => { 43 | expect(() => { 44 | get(poolAddress); 45 | }).toThrow(); 46 | }); 47 | 48 | it('should return pool contracts if defined', () => { 49 | const _object = { 50 | lendingPoolAddressProvider: poolAddress, 51 | incentiveControllers: ['123'], 52 | lendingPoolContract: {} as any, 53 | }; 54 | add(_object); 55 | expect(get(poolAddress)).toEqual(_object); 56 | }); 57 | }); 58 | describe('add', () => { 59 | it('should add a pool contract', () => { 60 | const _object = { 61 | lendingPoolAddressProvider: poolAddress, 62 | incentiveControllers: ['123'], 63 | lendingPoolContract: {} as any, 64 | }; 65 | add(_object); 66 | expect(get(poolAddress)).toEqual(_object); 67 | }); 68 | 69 | it('should add a pool contracts', () => { 70 | const _object = { 71 | lendingPoolAddressProvider: poolAddress, 72 | incentiveControllers: ['123'], 73 | lendingPoolContract: {} as any, 74 | }; 75 | 76 | const _object2 = { 77 | lendingPoolAddressProvider: '123', 78 | incentiveControllers: ['123'], 79 | lendingPoolContract: {} as any, 80 | }; 81 | add(_object); 82 | add(_object2); 83 | expect(get(poolAddress)).toEqual(_object); 84 | expect(get('123')).toEqual(_object2); 85 | }); 86 | 87 | it('should add only 1 pool contracts', () => { 88 | const _object = { 89 | lendingPoolAddressProvider: poolAddress, 90 | incentiveControllers: ['123'], 91 | lendingPoolContract: {} as any, 92 | }; 93 | add(_object); 94 | add(_object); 95 | add(_object); 96 | expect(get(poolAddress)).toEqual(_object); 97 | }); 98 | }); 99 | 100 | describe('init', () => { 101 | const ethereumProviderMock = {} as any; 102 | const lendingPool = '0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9'; 103 | const reserveUsedAsCollateralDisabledMock = 'reserveUsedAsCollateralDisabledMock'; 104 | const reserveUsedAsCollateralEnabledMock = 'reserveUsedAsCollateralEnabledMock'; 105 | const ILendingPoolMock = { 106 | queryFilter: jest.fn().mockImplementation(() => [{ args: { user: userAddress } }]), 107 | filters: { 108 | ReserveUsedAsCollateralDisabled: jest 109 | .fn() 110 | .mockImplementation(() => reserveUsedAsCollateralDisabledMock), 111 | ReserveUsedAsCollateralEnabled: jest 112 | .fn() 113 | .mockImplementation(() => reserveUsedAsCollateralEnabledMock), 114 | }, 115 | }; 116 | 117 | beforeEach(() => { 118 | ILendingPoolAddressesProviderFactory.connect = jest.fn().mockImplementation(() => { 119 | return { 120 | getLendingPool: jest.fn().mockImplementation(() => lendingPool), 121 | }; 122 | }); 123 | 124 | ILendingPoolFactory.connect = jest.fn().mockImplementation(() => ILendingPoolMock); 125 | }); 126 | 127 | it('should connect to `ILendingPoolAddressesProviderFactory`', async () => { 128 | const spy = jest.spyOn(ILendingPoolAddressesProviderFactory, 'connect'); 129 | await init(poolAddress, ethereumProviderMock); 130 | expect(spy).toHaveBeenCalledTimes(1); 131 | expect(spy).toHaveBeenCalledWith(poolAddress, ethereumProviderMock); 132 | }); 133 | 134 | it('should connect to `ILendingPoolFactory`', async () => { 135 | const spy = jest.spyOn(ILendingPoolFactory, 'connect'); 136 | await init(poolAddress, ethereumProviderMock); 137 | expect(spy).toHaveBeenCalledTimes(1); 138 | expect(spy).toHaveBeenCalledWith(lendingPool, ethereumProviderMock); 139 | }); 140 | 141 | it('should call `getPoolIncentivesRPC`', async () => { 142 | const spy = jest.spyOn(rpc, 'getPoolIncentivesRPC'); 143 | await init(poolAddress, ethereumProviderMock); 144 | expect(spy).toHaveBeenCalledTimes(1); 145 | expect(spy).toHaveBeenCalledWith({ lendingPoolAddressProvider: poolAddress }); 146 | }); 147 | 148 | it('should add to pool contracts', async () => { 149 | await init(poolAddress, ethereumProviderMock); 150 | expect(get(poolAddress).lendingPoolAddressProvider).toEqual(poolAddress); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /backend/__tests__/tasks/update-users-incentives-data/pool-incentives-data.state.spec.ts: -------------------------------------------------------------------------------- 1 | import * as poolIncentivesDataProvider from '../../../src/services/incentives-data'; 2 | import { 3 | add, 4 | fetchAndAdd, 5 | get, 6 | } from '../../../src/tasks/update-users-incentives-data/pool-incentives-data.state'; 7 | import { poolAddress } from '../../mocks'; 8 | 9 | const getProtocolDataMock = [ 10 | { 11 | aIncentiveData: { 12 | emissionEndTimestamp: 1, 13 | tokenAddress: 'aTokenAddressMock1', 14 | }, 15 | sIncentiveData: { 16 | emissionEndTimestamp: 1, 17 | tokenAddress: 'stableDebtTokenAddressMock1', 18 | }, 19 | vIncentiveData: { 20 | emissionEndTimestamp: 1, 21 | tokenAddress: 'variableDebtTokenAddressMock1', 22 | }, 23 | }, 24 | { 25 | emissionEndTimestamp: 1, 26 | aIncentiveData: { 27 | emissionEndTimestamp: 1, 28 | tokenAddress: 'aTokenAddressMock2', 29 | }, 30 | sIncentiveData: { 31 | emissionEndTimestamp: 1, 32 | tokenAddress: 'stableDebtTokenAddressMock2', 33 | }, 34 | vIncentiveData: { 35 | emissionEndTimestamp: 1, 36 | tokenAddress: 'variableDebtTokenAddressMock2', 37 | }, 38 | }, 39 | ]; 40 | 41 | jest.mock('../../../src/services/incentives-data', () => ({ 42 | __esModule: true, 43 | getPoolIncentives: jest.fn().mockImplementation(() => getProtocolDataMock), 44 | })); 45 | 46 | describe('poolIncentivesState', () => { 47 | describe('fetchAndAdd', () => { 48 | it('should call `getPoolIncentives`', async () => { 49 | const spy = jest.spyOn(poolIncentivesDataProvider, 'getPoolIncentives'); 50 | await fetchAndAdd(poolAddress); 51 | expect(spy).toHaveBeenCalledTimes(1); 52 | expect(spy).toHaveBeenCalledWith({ lendingPoolAddressProvider: poolAddress }); 53 | }); 54 | 55 | it('should add reserve lists for pool', async () => { 56 | const spy = jest.spyOn(poolIncentivesDataProvider, 'getPoolIncentives'); 57 | const _poolAddress = 'POOOOOOOOOOL'; 58 | const list = await fetchAndAdd(_poolAddress); 59 | expect(spy).toHaveBeenCalledTimes(1); 60 | expect(spy).toHaveBeenCalledWith({ lendingPoolAddressProvider: _poolAddress }); 61 | expect(get(_poolAddress)).toEqual(list); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /backend/babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 4 | plugins: [['@babel/plugin-proposal-decorators', { legacy: true, loose: true }]], 5 | }; 6 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "exec": "ts-node ./src/api.ts" 5 | } 6 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aave-client-caching-server", 3 | "version": "0.0.1", 4 | "private": false, 5 | "scripts": { 6 | "start": "npm i && nodemon", 7 | "test": "jest --runInBand --detectOpenHandles", 8 | "prod": "ts-node ./src/api.ts", 9 | "job:update-block-number": "ts-node ./src/tasks/update-block-number/run.ts", 10 | "job:update-general-reserves-data": "ts-node ./src/tasks/update-general-reserves-data/run.ts", 11 | "job:update-users-data": "ts-node ./src/tasks/update-users-data/run.ts", 12 | "job:update-stake-user-ui-data": "ts-node ./src/tasks/update-stake-user-ui-data/run.ts", 13 | "job:update-stake-general-ui-data": "ts-node ./src/tasks/update-stake-general-ui-data/run.ts", 14 | "job:update-users-incentives-data": "ts-node ./src/tasks/update-users-incentives-data/run.ts", 15 | "job:update-reserve-incentives-data": "ts-node ./src/tasks/update-reserve-incentives-data/run.ts" 16 | }, 17 | "dependencies": { 18 | "@aave/contract-helpers": "1.3.2", 19 | "@aave/protocol-js": "^3.0.0", 20 | "@alch/alchemy-web3": "^1.1.9", 21 | "apollo-server-express": "^3.5.0", 22 | "class-validator": "^0.13.2", 23 | "ethers": "^5.5.2", 24 | "express": "^4.17.1", 25 | "graphql": "^15.5.0", 26 | "graphql-redis-subscriptions": "^2.4.2", 27 | "graphql-ws": "^5.5.5", 28 | "ioredis": "^4.28.2", 29 | "reflect-metadata": "^0.1.13", 30 | "type-graphql": "^1.1.1", 31 | "ws": "^8.3.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.16.5", 35 | "@babel/plugin-proposal-decorators": "^7.16.5", 36 | "@babel/preset-env": "^7.16.5", 37 | "@babel/preset-typescript": "^7.16.5", 38 | "@types/ioredis": "^4.28.4", 39 | "@types/jest": "^27.0.3", 40 | "@types/node": "^16.11.13", 41 | "@typescript-eslint/eslint-plugin": "^5.7.0", 42 | "@typescript-eslint/parser": "^5.7.0", 43 | "babel-jest": "^27.0.6", 44 | "eslint": "^8.4.1", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-plugin-prettier": "^4.0.0", 47 | "husky": "^7.0.4", 48 | "jest": "^27.4.5", 49 | "lint-staged": "^12.1.2", 50 | "nodemon": "^2.0.15", 51 | "prettier": "^2.5.1", 52 | "ts-node": "^10.4.0", 53 | "typescript": "^4.5.4" 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": "export CI=true && lint-staged", 58 | "pre-push": "export CI=true && lint-staged" 59 | } 60 | }, 61 | "lint-staged": { 62 | "src/**/*.{ts,tsx,json,sass,md}": [ 63 | "prettier --single-quote --write", 64 | "git add" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/api.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-express'; 2 | import 'reflect-metadata'; 3 | import { buildSchema, NonEmptyArray } from 'type-graphql'; 4 | import { 5 | HealthResolver, 6 | ProtocolDataResolver, 7 | IncentivesDataResolver, 8 | StakeDataResolver, 9 | } from './graphql/resolvers'; 10 | import { getPubSub } from './pubsub'; 11 | import { isStakeEnabled } from './tasks/task-helpers'; 12 | import express from 'express'; 13 | import * as WebSocket from 'ws'; 14 | import { useServer } from 'graphql-ws/lib/use/ws'; 15 | 16 | const PORT = process.env.PORT || 3000; 17 | 18 | async function bootstrap() { 19 | const resolvers: NonEmptyArray | NonEmptyArray = isStakeEnabled() 20 | ? [ProtocolDataResolver, HealthResolver, IncentivesDataResolver, StakeDataResolver] 21 | : [ProtocolDataResolver, IncentivesDataResolver, HealthResolver]; 22 | 23 | const schema = await buildSchema({ 24 | resolvers, 25 | pubSub: getPubSub(), 26 | }); 27 | 28 | const app = express(); 29 | 30 | // Create the GraphQL server 31 | const server = new ApolloServer({ 32 | schema, 33 | }); 34 | 35 | await server.start(); 36 | 37 | server.applyMiddleware({ app }); 38 | 39 | try { 40 | // Start the server 41 | const server = await app.listen(PORT, () => { 42 | const wsServer = new WebSocket.Server({ 43 | server, 44 | path: '/graphql', 45 | }); 46 | useServer({ schema }, wsServer); 47 | }); 48 | } catch (e) { 49 | console.error('Apollo server exited with', e); 50 | process.exit(1); 51 | } 52 | } 53 | 54 | bootstrap(); 55 | -------------------------------------------------------------------------------- /backend/src/config.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '@aave/contract-helpers'; 2 | import { getParam, getParamOrExit } from './env-helpers'; 3 | 4 | export const REDIS_HOST = getParamOrExit('REDIS_HOST'); 5 | 6 | export const RPC_URL = getParamOrExit('RPC_URL'); 7 | const _BACKUP_RPC_URLS: string | null = getParam('BACKUP_RPC_URLS'); 8 | const _parseBackupRpcUrls = (value: string) => { 9 | if (value.length < 1) { 10 | return []; 11 | } 12 | return value.split(','); 13 | }; 14 | export const BACKUP_RPC_URLS = 15 | _BACKUP_RPC_URLS === null ? [] : _parseBackupRpcUrls(_BACKUP_RPC_URLS); 16 | 17 | export const RPC_MAX_TIMEOUT = 5000; 18 | 19 | export const CHAIN_ID = Number(process.env.CHAIN_ID || '0') as 20 | | ChainId.mainnet 21 | | ChainId.polygon 22 | | ChainId.avalanche; 23 | 24 | if (![ChainId.mainnet, ChainId.polygon, ChainId.avalanche].includes(CHAIN_ID)) { 25 | throw new Error(`ChainId: "${CHAIN_ID}" is not currently supported`); 26 | } 27 | 28 | const CONFIGS: { 29 | [key: number]: { 30 | PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES: string[]; 31 | PROTOCOLS_WITH_INCENTIVES_ADDRESSES: string[]; 32 | POOL_UI_DATA_PROVIDER_ADDRESS: string; 33 | UI_INCENTIVE_DATA_PROVIDER_ADDRESS: string; 34 | GENERAL_RESERVES_DATA_POOLING_INTERVAL: number; 35 | USERS_DATA_POOLING_INTERVAL: number; 36 | RESERVE_INCENTIVES_DATA_POOLING_INTERVAL: number; 37 | USER_INCENTIVES_DATA_POOLING_INTERVAL: number; 38 | BLOCK_NUMBER_POOLING_INTERVAL: number; 39 | }; 40 | } = { 41 | [ChainId.mainnet]: { 42 | BLOCK_NUMBER_POOLING_INTERVAL: 1000, 43 | PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES: [ 44 | '0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5', 45 | '0xacc030ef66f9dfeae9cbb0cd1b25654b82cfa8d5', 46 | ], 47 | PROTOCOLS_WITH_INCENTIVES_ADDRESSES: ['0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5'], 48 | POOL_UI_DATA_PROVIDER_ADDRESS: '0x548e95Ce38B8cb1D91FD82A9F094F26295840277', 49 | UI_INCENTIVE_DATA_PROVIDER_ADDRESS: '0xD01ab9a6577E1D84F142e44D49380e23A340387d', 50 | GENERAL_RESERVES_DATA_POOLING_INTERVAL: 4000, 51 | USERS_DATA_POOLING_INTERVAL: 4000, 52 | RESERVE_INCENTIVES_DATA_POOLING_INTERVAL: 4000, 53 | USER_INCENTIVES_DATA_POOLING_INTERVAL: 4000, 54 | }, 55 | [ChainId.polygon]: { 56 | BLOCK_NUMBER_POOLING_INTERVAL: 5000, 57 | PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES: ['0xd05e3E715d945B59290df0ae8eF85c1BdB684744'], 58 | PROTOCOLS_WITH_INCENTIVES_ADDRESSES: ['0xd05e3E715d945B59290df0ae8eF85c1BdB684744'], 59 | POOL_UI_DATA_PROVIDER_ADDRESS: '0x67acdB3469580185811E5769113509c6e8B6Cba5', 60 | UI_INCENTIVE_DATA_PROVIDER_ADDRESS: '0x645654D59A5226CBab969b1f5431aA47CBf64ab8', 61 | GENERAL_RESERVES_DATA_POOLING_INTERVAL: 2000, 62 | USERS_DATA_POOLING_INTERVAL: 2000, 63 | RESERVE_INCENTIVES_DATA_POOLING_INTERVAL: 2000, 64 | USER_INCENTIVES_DATA_POOLING_INTERVAL: 2000, 65 | }, 66 | [ChainId.avalanche]: { 67 | BLOCK_NUMBER_POOLING_INTERVAL: 5000, 68 | PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES: ['0xb6A86025F0FE1862B372cb0ca18CE3EDe02A318f'], 69 | PROTOCOLS_WITH_INCENTIVES_ADDRESSES: ['0xb6A86025F0FE1862B372cb0ca18CE3EDe02A318f'], 70 | POOL_UI_DATA_PROVIDER_ADDRESS: '0x88be7eC36719fadAbdE4307ec61EAB6fda788CEF', 71 | UI_INCENTIVE_DATA_PROVIDER_ADDRESS: '0x11979886A6dBAE27D7a72c49fCF3F23240D647bF', 72 | GENERAL_RESERVES_DATA_POOLING_INTERVAL: 1000, 73 | USERS_DATA_POOLING_INTERVAL: 1000, 74 | RESERVE_INCENTIVES_DATA_POOLING_INTERVAL: 1000, 75 | USER_INCENTIVES_DATA_POOLING_INTERVAL: 1000, 76 | }, 77 | }; 78 | 79 | const _STAKE_CONFIG = { 80 | STAKE_DATA_PROVIDER: '0xc57450af527d10Fe182521AB39C1AD23c1e1BaDE', 81 | STK_AAVE_TOKEN_ADDRESS: '0x4da27a545c0c5B758a6BA100e3a049001de870f5', 82 | STK_ABPT_TOKEN_ADDRESS: '0xa1116930326D21fB917d5A27F1E9943A9595fb47', 83 | ABPT_TOKEN: '0x41A08648C3766F9F9d85598fF102a08f4ef84F84', 84 | AAVE_TOKEN_ADDRESS: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', 85 | STAKE_DATA_POOLING_INTERVAL: 4000, 86 | }; 87 | 88 | export const CONFIG = CONFIGS[CHAIN_ID]; 89 | 90 | export const STAKING_CONFIG: typeof _STAKE_CONFIG | undefined = 91 | CHAIN_ID === 1 ? _STAKE_CONFIG : undefined; 92 | 93 | export const RESERVES_LIST_VALIDITY_INTERVAL = 60 * 5 * 1000; 94 | -------------------------------------------------------------------------------- /backend/src/custom-provider/aave-provider-manager.ts: -------------------------------------------------------------------------------- 1 | import { AaveCustomProvider } from './aave-custom-provider'; 2 | import { AaveProviderContext } from './models/aave-provider-context'; 3 | import { InternalAaveProviderContext } from './models/internal-aave-provider-context'; 4 | 5 | export const generate: (context: AaveProviderContext) => AaveCustomProvider = ( 6 | context: AaveProviderContext 7 | ) => { 8 | return new AaveCustomProvider(context, internalGenerate); 9 | }; 10 | 11 | const internalGenerate: (context: InternalAaveProviderContext) => AaveCustomProvider = ( 12 | context: AaveProviderContext 13 | ) => { 14 | return new AaveCustomProvider(context, internalGenerate); 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/custom-provider/models/aave-provider-context.ts: -------------------------------------------------------------------------------- 1 | import { BaseProviderContext } from './base-provider-context'; 2 | 3 | export interface AaveProviderContext extends BaseProviderContext {} 4 | -------------------------------------------------------------------------------- /backend/src/custom-provider/models/already-used-node-context.ts: -------------------------------------------------------------------------------- 1 | export interface AlreadyUsedNodeContext { 2 | wasMainNode: boolean; 3 | node: string; 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/custom-provider/models/base-provider-context.ts: -------------------------------------------------------------------------------- 1 | import { mainNodeReconnectionSettings } from './main-node-reconnection-settings'; 2 | 3 | export interface BaseProviderContext { 4 | selectedNode: string; 5 | backupNodes: string[]; 6 | maxTimout?: number | undefined; 7 | mainNodeReconnectionSettings?: mainNodeReconnectionSettings[] | undefined; 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/custom-provider/models/internal-aave-provider-context.ts: -------------------------------------------------------------------------------- 1 | import { AlreadyUsedNodeContext } from './already-used-node-context'; 2 | import { BaseProviderContext } from './base-provider-context'; 3 | 4 | export interface InternalAaveProviderContext extends BaseProviderContext { 5 | alreadyUsedNodes: AlreadyUsedNodeContext[]; 6 | mainNode: string; 7 | mainNodeReconnectionContext?: 8 | | { 9 | reconnectAttempts: number; 10 | lastAttempted: number; 11 | } 12 | | undefined; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/custom-provider/models/main-node-reconnection-settings.ts: -------------------------------------------------------------------------------- 1 | export interface mainNodeReconnectionSettings { 2 | reconnectAttempts: number; 3 | reconnectIntervalAttempts: number; 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/env-helpers.ts: -------------------------------------------------------------------------------- 1 | export function getParamOrExit(name: string): string { 2 | const param = process.env[name]; 3 | if (!param) { 4 | console.error('Required config param missing:', name); 5 | process.exit(1); 6 | } 7 | return param; 8 | } 9 | 10 | export function getParam(name: string): string | null { 11 | const param = process.env[name]; 12 | if (!param) { 13 | return null; 14 | } 15 | return param; 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/graphql/object-types/incentives.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class RewardInfo { 5 | @Field() 6 | rewardTokenSymbol: string; 7 | 8 | @Field() 9 | rewardTokenAddress: string; 10 | 11 | @Field() 12 | rewardOracleAddress: string; 13 | 14 | @Field() 15 | emissionPerSecond: string; 16 | 17 | @Field() 18 | incentivesLastUpdateTimestamp: number; 19 | 20 | @Field() 21 | tokenIncentivesIndex: string; 22 | 23 | @Field() 24 | emissionEndTimestamp: number; 25 | 26 | @Field() 27 | rewardPriceFeed: string; 28 | 29 | @Field() 30 | rewardTokenDecimals: number; 31 | 32 | @Field() 33 | precision: number; 34 | 35 | @Field() 36 | priceFeedDecimals: number; 37 | } 38 | 39 | @ObjectType() 40 | export class IncentiveData { 41 | @Field() 42 | tokenAddress: string; 43 | 44 | @Field() 45 | incentiveControllerAddress: string; 46 | 47 | @Field(() => [RewardInfo]) 48 | rewardsTokenInformation: RewardInfo[]; 49 | } 50 | 51 | @ObjectType() 52 | export class ReserveIncentivesData { 53 | @Field() 54 | id: string; 55 | 56 | @Field() 57 | underlyingAsset: string; 58 | 59 | @Field(() => IncentiveData) 60 | aIncentiveData: IncentiveData; 61 | 62 | @Field(() => IncentiveData) 63 | vIncentiveData: IncentiveData; 64 | 65 | @Field(() => IncentiveData) 66 | sIncentiveData: IncentiveData; 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/graphql/object-types/reserve.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class ReserveData { 5 | @Field() 6 | id: string; 7 | 8 | @Field() 9 | underlyingAsset: string; 10 | 11 | @Field() 12 | name: string; 13 | 14 | @Field() 15 | symbol: string; 16 | 17 | @Field() 18 | decimals: number; 19 | 20 | @Field() 21 | isActive: boolean; 22 | 23 | @Field() 24 | isFrozen: boolean; 25 | 26 | @Field() 27 | usageAsCollateralEnabled: boolean; 28 | 29 | @Field() 30 | aTokenAddress: string; 31 | 32 | @Field() 33 | stableDebtTokenAddress: string; 34 | 35 | @Field() 36 | variableDebtTokenAddress: string; 37 | 38 | @Field() 39 | borrowingEnabled: boolean; 40 | 41 | @Field() 42 | stableBorrowRateEnabled: boolean; 43 | 44 | @Field() 45 | reserveFactor: string; 46 | 47 | @Field() 48 | baseLTVasCollateral: string; 49 | 50 | @Field() 51 | stableRateSlope1: string; 52 | 53 | @Field() 54 | stableRateSlope2: string; 55 | 56 | @Field() 57 | averageStableRate: string; 58 | 59 | @Field() 60 | stableDebtLastUpdateTimestamp: number; 61 | 62 | @Field() 63 | variableRateSlope1: string; 64 | 65 | @Field() 66 | variableRateSlope2: string; 67 | 68 | @Field() 69 | baseStableBorrowRate: string; 70 | 71 | @Field() 72 | baseVariableBorrowRate: string; 73 | 74 | @Field() 75 | optimalUsageRatio: string; 76 | 77 | @Field() 78 | liquidityIndex: string; 79 | 80 | @Field() 81 | reserveLiquidationThreshold: string; 82 | 83 | @Field() 84 | reserveLiquidationBonus: string; 85 | 86 | @Field() 87 | variableBorrowIndex: string; 88 | 89 | @Field() 90 | variableBorrowRate: string; 91 | 92 | @Field() 93 | availableLiquidity: string; 94 | 95 | @Field() 96 | stableBorrowRate: string; 97 | 98 | @Field() 99 | liquidityRate: string; 100 | 101 | @Field() 102 | totalPrincipalStableDebt: string; 103 | 104 | @Field() 105 | totalScaledVariableDebt: string; 106 | 107 | @Field() 108 | lastUpdateTimestamp: number; 109 | 110 | @Field() 111 | priceInMarketReferenceCurrency: string; 112 | 113 | @Field() 114 | priceOracle: string; 115 | 116 | @Field() 117 | interestRateStrategyAddress: string; 118 | 119 | // V3 specific 120 | 121 | @Field() 122 | isPaused: boolean; 123 | 124 | @Field() 125 | accruedToTreasury: string; 126 | 127 | @Field() 128 | unbacked: string; 129 | 130 | @Field() 131 | isolationModeTotalDebt: string; 132 | 133 | @Field() 134 | debtCeiling: string; 135 | 136 | @Field() 137 | debtCeilingDecimals: number; 138 | 139 | @Field() 140 | eModeCategoryId: number; 141 | 142 | @Field() 143 | borrowCap: string; 144 | 145 | @Field() 146 | supplyCap: string; 147 | 148 | @Field() 149 | eModeLtv: number; 150 | 151 | @Field() 152 | eModeLiquidationThreshold: number; 153 | 154 | @Field() 155 | eModeLiquidationBonus: number; 156 | 157 | @Field() 158 | eModePriceSource: string; 159 | 160 | @Field() 161 | eModeLabel: string; 162 | 163 | @Field() 164 | borrowableInIsolation: boolean; 165 | } 166 | 167 | @ObjectType() 168 | export class BaseCurrencyData { 169 | @Field() 170 | marketReferenceCurrencyDecimals: number; 171 | 172 | @Field() 173 | marketReferenceCurrencyPriceInUsd: string; 174 | 175 | @Field() 176 | networkBaseTokenPriceInUsd: string; 177 | 178 | @Field() 179 | networkBaseTokenPriceDecimals: number; 180 | } 181 | 182 | @ObjectType() 183 | export class ProtocolData { 184 | @Field(() => [ReserveData]) 185 | reserves: ReserveData[]; 186 | 187 | @Field(() => BaseCurrencyData) 188 | baseCurrencyData: BaseCurrencyData; 189 | } 190 | -------------------------------------------------------------------------------- /backend/src/graphql/object-types/stake-general-data.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class StakeGeneralData { 5 | @Field() 6 | stakeTokenTotalSupply: string; 7 | 8 | @Field() 9 | stakeCooldownSeconds: number; 10 | 11 | @Field() 12 | stakeUnstakeWindow: number; 13 | 14 | @Field() 15 | stakeTokenPriceEth: string; 16 | 17 | @Field() 18 | rewardTokenPriceEth: string; 19 | 20 | @Field() 21 | stakeApy: string; 22 | 23 | @Field() 24 | distributionPerSecond: string; 25 | 26 | @Field() 27 | distributionEnd: string; 28 | } 29 | 30 | @ObjectType() 31 | export class StakeGeneralUIData { 32 | @Field(() => StakeGeneralData) 33 | aave: StakeGeneralData; 34 | 35 | @Field(() => StakeGeneralData) 36 | bpt: StakeGeneralData; 37 | 38 | @Field() 39 | usdPriceEth: string; 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/graphql/object-types/stake-user-data.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class StakeUserData { 5 | @Field() 6 | stakeTokenUserBalance: string; 7 | 8 | @Field() 9 | underlyingTokenUserBalance: string; 10 | 11 | @Field() 12 | userCooldown: number; 13 | 14 | @Field() 15 | userIncentivesToClaim: string; 16 | 17 | @Field() 18 | userPermitNonce: string; 19 | } 20 | 21 | @ObjectType() 22 | export class StakeUserUIData { 23 | @Field(() => StakeUserData) 24 | aave: StakeUserData; 25 | 26 | @Field(() => StakeUserData) 27 | bpt: StakeUserData; 28 | 29 | @Field() 30 | usdPriceEth: string; 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/graphql/object-types/user-incentives.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class UserRewardInfo { 5 | @Field() 6 | rewardTokenSymbol: string; 7 | 8 | @Field() 9 | rewardOracleAddress: string; 10 | 11 | @Field() 12 | rewardTokenAddress: string; 13 | 14 | @Field() 15 | userUnclaimedRewards: string; 16 | 17 | @Field() 18 | tokenIncentivesUserIndex: string; 19 | 20 | @Field() 21 | rewardPriceFeed: string; 22 | 23 | @Field() 24 | priceFeedDecimals: number; 25 | 26 | @Field() 27 | rewardTokenDecimals: number; 28 | } 29 | 30 | @ObjectType() 31 | export class UserIncentiveData { 32 | @Field() 33 | tokenAddress: string; 34 | 35 | @Field() 36 | incentiveControllerAddress: string; 37 | 38 | @Field(() => [UserRewardInfo]) 39 | userRewardsInformation: UserRewardInfo[]; 40 | } 41 | 42 | @ObjectType() 43 | export class UserIncentivesData { 44 | @Field() 45 | id: string; 46 | 47 | @Field() 48 | underlyingAsset: string; 49 | 50 | @Field(() => UserIncentiveData) 51 | aTokenIncentivesUserData: UserIncentiveData; 52 | 53 | @Field(() => UserIncentiveData) 54 | vTokenIncentivesUserData: UserIncentiveData; 55 | 56 | @Field(() => UserIncentiveData) 57 | sTokenIncentivesUserData: UserIncentiveData; 58 | } 59 | -------------------------------------------------------------------------------- /backend/src/graphql/object-types/user-reserve.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class UserReserveData { 5 | @Field() 6 | id: string; 7 | 8 | @Field() 9 | underlyingAsset: string; 10 | 11 | @Field() 12 | scaledATokenBalance: string; 13 | 14 | @Field() 15 | usageAsCollateralEnabledOnUser: boolean; 16 | 17 | @Field() 18 | scaledVariableDebt: string; 19 | 20 | @Field() 21 | stableBorrowRate: string; 22 | 23 | @Field() 24 | principalStableDebt: string; 25 | 26 | @Field() 27 | stableBorrowLastUpdateTimestamp: number; 28 | } 29 | 30 | @ObjectType() 31 | export class UserReservesData { 32 | @Field(() => [UserReserveData]) 33 | userReserves: UserReserveData[]; 34 | 35 | @Field() 36 | userEmodeCategoryId: number; 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/graphql/resolvers/health-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from 'type-graphql'; 2 | 3 | @Resolver() 4 | export class HealthResolver { 5 | @Query(() => String) 6 | ping(): string { 7 | return 'pong'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/graphql/resolvers/incentives-data-resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Args, 3 | ArgsType, 4 | Field, 5 | Query, 6 | Resolver, 7 | ResolverFilterData, 8 | Root, 9 | Subscription, 10 | Int, 11 | } from 'type-graphql'; 12 | import { IncentivesDataPayload, Topics, UserIncentivesDataPayload } from '../../pubsub'; 13 | import { getPoolIncentives, getUserPoolIncentives } from '../../services/incentives-data'; 14 | import { ReserveIncentivesData } from '../object-types/incentives'; 15 | import { UserIncentivesData } from '../object-types/user-incentives'; 16 | import { IsEthAddress } from '../validators'; 17 | 18 | @ArgsType() 19 | class PoolArgs { 20 | @Field() 21 | @IsEthAddress() 22 | lendingPoolAddressProvider: string; 23 | 24 | @Field((type) => Int) 25 | chainId: number; 26 | } 27 | 28 | @ArgsType() 29 | class UserArgs extends PoolArgs { 30 | @Field() 31 | @IsEthAddress() 32 | userAddress: string; 33 | } 34 | 35 | @Resolver() 36 | export class IncentivesDataResolver { 37 | @Query(() => [ReserveIncentivesData]) 38 | async reservesIncentives( 39 | @Args() { lendingPoolAddressProvider }: PoolArgs 40 | ): Promise { 41 | return getPoolIncentives(lendingPoolAddressProvider); 42 | } 43 | 44 | @Subscription(() => [ReserveIncentivesData], { 45 | topics: Topics.INCENTIVES_DATA_UPDATE, 46 | filter: ({ payload, args }: ResolverFilterData) => 47 | payload.lendingPoolAddressProvider.toLowerCase() === 48 | args.lendingPoolAddressProvider.toLowerCase(), 49 | }) 50 | async poolIncentivesDataUpdate( 51 | @Root() data: IncentivesDataPayload, 52 | @Args() args: PoolArgs 53 | ): Promise { 54 | return data.incentivesData; 55 | } 56 | 57 | @Query(() => [UserIncentivesData]) 58 | async userIncentives( 59 | @Args() { userAddress, lendingPoolAddressProvider }: UserArgs 60 | ): Promise { 61 | return getUserPoolIncentives(lendingPoolAddressProvider, userAddress); 62 | } 63 | 64 | @Subscription(() => [UserIncentivesData], { 65 | topics: Topics.USER_INCENTIVES_DATA_UPDATE, 66 | filter: ({ payload, args }: ResolverFilterData) => 67 | payload.lendingPoolAddressProvider.toLowerCase() === 68 | args.lendingPoolAddressProvider.toLowerCase() && 69 | payload.userAddress.toLowerCase() === args.userAddress.toLowerCase(), 70 | }) 71 | async userPoolIncentivesDataUpdate( 72 | @Root() { userAddress, lendingPoolAddressProvider }: UserArgs, 73 | @Args() args: UserArgs 74 | ): Promise { 75 | return getUserPoolIncentives(lendingPoolAddressProvider, userAddress); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/graphql/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './health-resolver'; 2 | export * from './protocol-data-resolver'; 3 | export * from './stake-data-resolver'; 4 | export * from './incentives-data-resolver'; 5 | -------------------------------------------------------------------------------- /backend/src/graphql/resolvers/protocol-data-resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Args, 3 | ArgsType, 4 | Field, 5 | Int, 6 | Query, 7 | Resolver, 8 | ResolverFilterData, 9 | Root, 10 | Subscription, 11 | } from 'type-graphql'; 12 | import { ProtocolDataPayload, Topics, UserDataPayload } from '../../pubsub'; 13 | import { getProtocolData, getProtocolUserData } from '../../services/pool-data'; 14 | import { ProtocolData } from '../object-types/reserve'; 15 | import { UserReservesData } from '../object-types/user-reserve'; 16 | import { IsEthAddress } from '../validators'; 17 | 18 | @ArgsType() 19 | class ProtocolArgs { 20 | @Field() 21 | @IsEthAddress() 22 | lendingPoolAddressProvider: string; 23 | 24 | @Field((type) => Int) 25 | chainId: number; 26 | } 27 | 28 | @ArgsType() 29 | class UserArgs extends ProtocolArgs { 30 | @Field() 31 | @IsEthAddress() 32 | userAddress: string; 33 | } 34 | 35 | @Resolver() 36 | export class ProtocolDataResolver { 37 | @Query(() => ProtocolData) 38 | async protocolData(@Args() { lendingPoolAddressProvider }: ProtocolArgs): Promise { 39 | return getProtocolData(lendingPoolAddressProvider); 40 | } 41 | 42 | @Subscription(() => ProtocolData, { 43 | topics: Topics.PROTOCOL_DATA_UPDATE, 44 | filter: ({ payload, args }: ResolverFilterData) => 45 | payload.lendingPoolAddressProvider.toLowerCase() === 46 | args.lendingPoolAddressProvider.toLowerCase(), 47 | }) 48 | async protocolDataUpdate( 49 | @Root() data: ProtocolDataPayload, 50 | @Args() args: ProtocolArgs 51 | ): Promise { 52 | return data.protocolData; 53 | } 54 | 55 | @Query(() => UserReservesData) 56 | async userData( 57 | @Args() 58 | { userAddress, lendingPoolAddressProvider }: UserArgs 59 | ): Promise { 60 | return getProtocolUserData(lendingPoolAddressProvider, userAddress); 61 | } 62 | 63 | @Subscription(() => UserReservesData, { 64 | topics: Topics.USER_DATA_UPDATE, 65 | filter: ({ payload, args }: ResolverFilterData) => 66 | payload.lendingPoolAddressProvider.toLowerCase() === 67 | args.lendingPoolAddressProvider.toLowerCase() && 68 | payload.userAddress.toLowerCase() === args.userAddress.toLowerCase(), 69 | }) 70 | async userDataUpdate( 71 | @Root() { userAddress, lendingPoolAddressProvider }: UserDataPayload, 72 | @Args() args: UserArgs 73 | ): Promise { 74 | return getProtocolUserData(lendingPoolAddressProvider, userAddress, false); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/graphql/resolvers/stake-data-resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Args, 3 | ArgsType, 4 | Field, 5 | Int, 6 | Query, 7 | Resolver, 8 | ResolverFilterData, 9 | Root, 10 | Subscription, 11 | } from 'type-graphql'; 12 | import { StakeUserUIDataPayload, Topics } from '../../pubsub'; 13 | import { getStakeGeneralUIData, getStakeUserUIData } from '../../services/stake-data'; 14 | import { StakeGeneralUIData } from '../object-types/stake-general-data'; 15 | import { StakeUserUIData } from '../object-types/stake-user-data'; 16 | import { IsEthAddress } from '../validators'; 17 | 18 | @ArgsType() 19 | class UserArgs { 20 | @Field() 21 | @IsEthAddress() 22 | userAddress: string; 23 | 24 | @Field((type) => Int) 25 | chainId: number; 26 | } 27 | 28 | @Resolver() 29 | export class StakeDataResolver { 30 | @Query(() => StakeUserUIData) 31 | async stakeUserUIData( 32 | @Args() 33 | { userAddress }: UserArgs 34 | ): Promise { 35 | return getStakeUserUIData(userAddress); 36 | } 37 | 38 | @Subscription(() => StakeUserUIData, { 39 | topics: Topics.STAKE_USER_UI_DATA, 40 | filter: ({ payload, args }: ResolverFilterData) => 41 | payload.userAddress.toLowerCase() === args.userAddress.toLowerCase(), 42 | }) 43 | async stakeUserUIDataUpdate( 44 | @Root() { userAddress }: StakeUserUIDataPayload, 45 | @Args() args: UserArgs 46 | ): Promise { 47 | return getStakeUserUIData(userAddress, true); 48 | } 49 | 50 | @Query(() => StakeGeneralUIData) 51 | async stakeGeneralUIData(): Promise { 52 | return getStakeGeneralUIData(); 53 | } 54 | 55 | @Subscription(() => StakeGeneralUIData, { 56 | topics: Topics.STAKE_GENERAL_UI_DATA, 57 | }) 58 | async stakeGeneralUIDataUpdate(): Promise { 59 | return getStakeGeneralUIData(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/graphql/validators/index.ts: -------------------------------------------------------------------------------- 1 | import { Denominations } from '@aave/contract-helpers'; 2 | import { registerDecorator, ValidationArguments, ValidationOptions } from 'class-validator'; 3 | import { isAddress } from 'ethers/lib/utils'; 4 | 5 | export function IsEthAddress(validationOptions?: ValidationOptions) { 6 | return function (object: unknown, propertyName: string) { 7 | registerDecorator({ 8 | propertyName, 9 | name: 'isEthAddress', 10 | target: object.constructor, 11 | options: validationOptions, 12 | validator: { 13 | validate(value: string, args: ValidationArguments) { 14 | return isAddress(value); 15 | }, 16 | defaultMessage(validationArguments?: ValidationArguments): string { 17 | return 'Is not valid Ethereum address'; 18 | }, 19 | }, 20 | }); 21 | }; 22 | } 23 | 24 | export function IsEthAddressOrNull(validationOptions?: ValidationOptions) { 25 | return function (object: unknown, propertyName: string) { 26 | registerDecorator({ 27 | propertyName, 28 | name: 'isEthAddress', 29 | target: object.constructor, 30 | options: validationOptions, 31 | validator: { 32 | validate(value: string, args: ValidationArguments) { 33 | return isAddress(value) || !value; 34 | }, 35 | defaultMessage(validationArguments?: ValidationArguments): string { 36 | return 'Is not valid Ethereum address'; 37 | }, 38 | }, 39 | }); 40 | }; 41 | } 42 | 43 | export function IsDenominationOrNull(validationOptions?: ValidationOptions) { 44 | return function (object: unknown, propertyName: string) { 45 | registerDecorator({ 46 | propertyName, 47 | name: 'isDenomination', 48 | target: object.constructor, 49 | options: validationOptions, 50 | validator: { 51 | validate(value: string, args: ValidationArguments) { 52 | return Denominations[value] != null || !value; 53 | }, 54 | defaultMessage(validationArguments?: ValidationArguments): string { 55 | return 'Is not valid Denomination'; 56 | }, 57 | }, 58 | }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/helpers/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { createAlchemyWeb3 } from '@alch/alchemy-web3'; 2 | import { ethers, providers, utils } from 'ethers'; 3 | import { RPC_URL } from '../config'; 4 | import { getBlockNumberRedis } from '../redis'; 5 | 6 | // too much too soon ;)! we will roll this in once happy 7 | // with other changes 8 | // export const ethereumProvider = generate({ 9 | // selectedNode: RPC_URL, 10 | // backupNodes: BACKUP_RPC_URLS, 11 | // maxTimout: RPC_MAX_TIMEOUT, 12 | // }); 13 | 14 | export const ethereumProvider = new providers.StaticJsonRpcProvider(RPC_URL); 15 | 16 | export const alchemyWeb3Provider = createAlchemyWeb3(RPC_URL); 17 | 18 | export async function getBlockNumber(useCache = true): Promise { 19 | if (useCache) { 20 | try { 21 | const blockNumberStr = await getBlockNumberRedis(); 22 | if (blockNumberStr) { 23 | return Number(blockNumberStr); 24 | } 25 | } catch (e) {} 26 | } 27 | 28 | return await getBlockNumberRpc(); 29 | } 30 | 31 | export const getBlockNumberRpc = async (): Promise => { 32 | try { 33 | return await ethereumProvider.getBlockNumber(); 34 | } catch (error) { 35 | console.log(error); 36 | return 0; 37 | } 38 | }; 39 | 40 | export const getUsersFromLogs = async ( 41 | reservesList: string[], 42 | fromBlock: number, 43 | toBlock: number, 44 | topics: string[] = ['Transfer(address,address,uint256)'] 45 | ): Promise => { 46 | if (reservesList.length === 0) return []; 47 | const rawLogs = await alchemyWeb3Provider.eth.getPastLogs({ 48 | fromBlock, 49 | toBlock, 50 | topics: topics.map((t) => utils.id(t)), 51 | address: reservesList, 52 | }); 53 | const users = new Set(); 54 | rawLogs.forEach((data) => { 55 | const logs = alchemyWeb3Provider.eth.abi.decodeLog( 56 | [ 57 | { 58 | type: 'address', 59 | name: 'from', 60 | indexed: true, 61 | }, 62 | { 63 | type: 'address', 64 | name: 'to', 65 | indexed: true, 66 | }, 67 | ], 68 | '', 69 | [data.topics[1], data.topics[2]] 70 | ); 71 | users.add(logs.from); 72 | users.add(logs.to); 73 | }); 74 | return [...users].filter((address) => address !== ethers.constants.AddressZero); 75 | }; 76 | -------------------------------------------------------------------------------- /backend/src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export const sleep = (milliseconds: number): Promise => { 4 | return new Promise((resolve) => setTimeout(resolve, milliseconds)); 5 | }; 6 | 7 | export const jsonParse = (str: string) => { 8 | return JSON.parse(str) as ParsedType; 9 | }; 10 | 11 | export const createHash = (data: any) => { 12 | return crypto.createHash('md5').update(JSON.stringify(data)).digest('base64'); 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/pubsub.ts: -------------------------------------------------------------------------------- 1 | import { RedisPubSub } from 'graphql-redis-subscriptions'; 2 | import { ReserveIncentivesData } from './graphql/object-types/incentives'; 3 | import { ProtocolData } from './graphql/object-types/reserve'; 4 | import { getRedis } from './redis/shared'; 5 | 6 | export enum Topics { 7 | PROTOCOL_DATA_UPDATE = 'PROTOCOL_DATA_UPDATE', 8 | USER_DATA_UPDATE = 'USER_DATA_UPDATE', 9 | INCENTIVES_DATA_UPDATE = 'INCENTIVES_DATA_UPDATE', 10 | USER_INCENTIVES_DATA_UPDATE = 'USER_INCENTIVES_DATA_UPDATE', 11 | STAKE_USER_UI_DATA = 'STAKE_USER_UI_DATA', 12 | STAKE_GENERAL_UI_DATA = 'STAKE_GENERAL_UI_DATA', 13 | } 14 | 15 | const pubSub = new RedisPubSub({ 16 | publisher: getRedis(), 17 | subscriber: getRedis(), 18 | }); 19 | 20 | export const getPubSub = () => pubSub; 21 | 22 | export interface IncentivesDataPayload { 23 | lendingPoolAddressProvider: string; 24 | incentivesData: ReserveIncentivesData[]; 25 | } 26 | 27 | export const pushUpdatedPoolIncentivesDataToSubscriptions = async ( 28 | lendingPoolAddressProvider: string, 29 | incentivesData: ReserveIncentivesData[] 30 | ) => 31 | await pubSub.publish(Topics.INCENTIVES_DATA_UPDATE, { 32 | lendingPoolAddressProvider, 33 | incentivesData, 34 | }); 35 | 36 | export interface UserIncentivesDataPayload { 37 | lendingPoolAddressProvider: string; 38 | userAddress: string; 39 | } 40 | 41 | export const pushUpdatedUserPoolIncentivesDataToSubscriptions = async ( 42 | lendingPoolAddressProvider: string, 43 | userAddress: string 44 | ) => 45 | await pubSub.publish(Topics.USER_INCENTIVES_DATA_UPDATE, { 46 | lendingPoolAddressProvider, 47 | userAddress, 48 | }); 49 | 50 | export interface ProtocolDataPayload { 51 | lendingPoolAddressProvider: string; 52 | protocolData: ProtocolData; 53 | } 54 | 55 | export const pushUpdatedReserveDataToSubscriptions = async ( 56 | lendingPoolAddressProvider: string, 57 | protocolData: ProtocolData 58 | ) => 59 | await pubSub.publish(Topics.PROTOCOL_DATA_UPDATE, { 60 | lendingPoolAddressProvider, 61 | protocolData, 62 | }); 63 | 64 | export interface UserDataPayload { 65 | lendingPoolAddressProvider: string; 66 | userAddress: string; 67 | } 68 | export const pushUpdatedUserReserveDataToSubscriptions = async ( 69 | lendingPoolAddressProvider: string, 70 | userAddress: string 71 | ) => 72 | await pubSub.publish(Topics.USER_DATA_UPDATE, { 73 | lendingPoolAddressProvider, 74 | userAddress, 75 | }); 76 | 77 | export interface StakeUserUIDataPayload { 78 | userAddress: string; 79 | } 80 | export const pushUpdatedStakeUserUIDataToSubscriptions = async (userAddress: string) => 81 | await pubSub.publish(Topics.STAKE_USER_UI_DATA, { 82 | userAddress, 83 | }); 84 | 85 | export const pushUpdatedStakeGeneralUIDataToSubscriptions = async () => 86 | await pubSub.publish(Topics.STAKE_GENERAL_UI_DATA, {}); 87 | -------------------------------------------------------------------------------- /backend/src/redis/block-number.ts: -------------------------------------------------------------------------------- 1 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 2 | 3 | const BLOCK_NUMBER_REDIS_KEY = 'BLOCK_NUMBER'; 4 | 5 | export const getBlockNumberRedis = async (): Promise => 6 | await getExpireDataInRedis(BLOCK_NUMBER_REDIS_KEY); 7 | 8 | export const setBlockNumberRedis = async (blockNumber: number): Promise => 9 | await setExpireDataInRedis(BLOCK_NUMBER_REDIS_KEY, blockNumber.toString(10), 20); 10 | -------------------------------------------------------------------------------- /backend/src/redis/index.ts: -------------------------------------------------------------------------------- 1 | export * from './block-number'; 2 | export * from './protocol-data'; 3 | export * from './protocol-user-data'; 4 | export * from './stake-general-ui-data'; 5 | export * from './stake-user-ui-data'; 6 | export * from './pool-incentives'; 7 | export * from './pool-user-incentives'; 8 | -------------------------------------------------------------------------------- /backend/src/redis/pool-incentives.ts: -------------------------------------------------------------------------------- 1 | import { ReserveIncentivesData } from '../graphql/object-types/incentives'; 2 | import { jsonParse } from '../helpers/utils'; 3 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 4 | 5 | export interface RedisPoolIncentivesData { 6 | data: ReserveIncentivesData[]; 7 | hash: string; 8 | } 9 | 10 | export const setPoolIncentivesDataRedis = async ( 11 | key: string, 12 | data: RedisPoolIncentivesData 13 | ): Promise => setExpireDataInRedis(key, JSON.stringify(data)); 14 | 15 | export const getPoolIncentivesDataRedis = async ( 16 | key: string 17 | ): Promise => { 18 | const protocolDataCachedStr = await getExpireDataInRedis(key); 19 | if (protocolDataCachedStr) { 20 | return jsonParse(protocolDataCachedStr); 21 | } 22 | 23 | return null; 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/redis/pool-user-incentives.ts: -------------------------------------------------------------------------------- 1 | import { UserIncentivesData } from '../graphql/object-types/user-incentives'; 2 | import { jsonParse } from '../helpers/utils'; 3 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 4 | 5 | export const setPoolIncentivesUserDataRedis = async ( 6 | key: string, 7 | userAddress: string, 8 | data: UserIncentivesData[] 9 | ) => setExpireDataInRedis(`${key}${userAddress}`, JSON.stringify(data)); 10 | 11 | export const getPoolIncentivesUserDataRedis = async ( 12 | key: string, 13 | userAddress: string 14 | ): Promise => { 15 | const userIncentivesDataCachedStr = await getExpireDataInRedis(`${key}${userAddress}`); 16 | 17 | if (userIncentivesDataCachedStr) { 18 | return jsonParse(userIncentivesDataCachedStr); 19 | } 20 | 21 | return null; 22 | }; 23 | -------------------------------------------------------------------------------- /backend/src/redis/protocol-data.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolData } from '../graphql/object-types/reserve'; 2 | import { jsonParse } from '../helpers/utils'; 3 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 4 | 5 | interface RedisProtcolPoolData { 6 | data: ProtocolData; 7 | hash: string; 8 | } 9 | 10 | export const setProtocolDataRedis = async ( 11 | poolAddress: string, 12 | data: RedisProtcolPoolData 13 | ): Promise => setExpireDataInRedis(poolAddress, JSON.stringify(data)); 14 | 15 | export const getProtocolDataRedis = async ( 16 | poolAddress: string 17 | ): Promise => { 18 | const protocolDataCachedStr = await getExpireDataInRedis(poolAddress); 19 | if (protocolDataCachedStr) { 20 | return jsonParse(protocolDataCachedStr); 21 | } 22 | 23 | return null; 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/redis/protocol-user-data.ts: -------------------------------------------------------------------------------- 1 | import { UserReservesData } from '../graphql/object-types/user-reserve'; 2 | import { jsonParse } from '../helpers/utils'; 3 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 4 | 5 | export const setProtocolUserDataRedis = async ( 6 | poolAddress: string, 7 | userAddress: string, 8 | data: UserReservesData 9 | ) => setExpireDataInRedis(`${poolAddress}${userAddress}`, JSON.stringify(data)); 10 | 11 | export const getProtocolUserDataRedis = async ( 12 | poolAddress: string, 13 | userAddress: string 14 | ): Promise => { 15 | const userDataCachedStr = await getExpireDataInRedis(`${poolAddress}${userAddress}`); 16 | 17 | if (userDataCachedStr) { 18 | return jsonParse(userDataCachedStr); 19 | } 20 | 21 | return null; 22 | }; 23 | -------------------------------------------------------------------------------- /backend/src/redis/shared.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | import { REDIS_HOST } from '../config'; 3 | 4 | export const getRedis = () => 5 | new Redis({ 6 | host: REDIS_HOST || 'redis', 7 | retryStrategy(times): number { 8 | return Math.max(times * 100, 3000); 9 | }, 10 | }); 11 | 12 | const cacheRedis = getRedis(); 13 | 14 | export const setExpireDataInRedis = async ( 15 | key: string, 16 | value: string, 17 | seconds = 60 18 | ): Promise => { 19 | return await cacheRedis.set(key, value, 'EX', seconds); 20 | }; 21 | 22 | export const getExpireDataInRedis = async (key: string): Promise => 23 | cacheRedis.get(key); 24 | -------------------------------------------------------------------------------- /backend/src/redis/stake-general-ui-data.ts: -------------------------------------------------------------------------------- 1 | import { StakeGeneralUIData } from '../graphql/object-types/stake-general-data'; 2 | import { jsonParse } from '../helpers/utils'; 3 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 4 | 5 | const KEY = 'stake-general-ui-data'; 6 | 7 | export const setStakeGeneralUIDataRedis = async ( 8 | data: StakeGeneralUIData 9 | ): Promise => setExpireDataInRedis(KEY, JSON.stringify(data)); 10 | 11 | export const getStakeGeneralUIDataRedis = async (): Promise => { 12 | const stakeUserDataCachedStr = await getExpireDataInRedis(KEY); 13 | if (stakeUserDataCachedStr) { 14 | return jsonParse(stakeUserDataCachedStr); 15 | } 16 | 17 | return null; 18 | }; 19 | -------------------------------------------------------------------------------- /backend/src/redis/stake-user-ui-data.ts: -------------------------------------------------------------------------------- 1 | import { StakeUserUIData } from '../graphql/object-types/stake-user-data'; 2 | import { jsonParse } from '../helpers/utils'; 3 | import { getExpireDataInRedis, setExpireDataInRedis } from './shared'; 4 | 5 | const buildKey = (userAddress: string) => `stake-user-ui-data:${userAddress}`; 6 | 7 | export const setStakeUserUIDataRedis = async ( 8 | userAddress: string, 9 | data: StakeUserUIData 10 | ): Promise => setExpireDataInRedis(buildKey(userAddress), JSON.stringify(data)); 11 | 12 | export const getStakeUserUIDataRedis = async ( 13 | userAddress: string 14 | ): Promise => { 15 | const stakeUserDataCachedStr = await getExpireDataInRedis(buildKey(userAddress)); 16 | if (stakeUserDataCachedStr) { 17 | return jsonParse(stakeUserDataCachedStr); 18 | } 19 | 20 | return null; 21 | }; 22 | -------------------------------------------------------------------------------- /backend/src/services/incentives-data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './rpc'; 3 | -------------------------------------------------------------------------------- /backend/src/services/incentives-data/provider.ts: -------------------------------------------------------------------------------- 1 | import { getPoolIncentivesRPC, getUserPoolIncentivesRPC } from '.'; 2 | import { ReserveIncentivesData } from '../../graphql/object-types/incentives'; 3 | import { UserIncentivesData } from '../../graphql/object-types/user-incentives'; 4 | import { createHash } from '../../helpers/utils'; 5 | import { 6 | getPoolIncentivesDataRedis, 7 | setPoolIncentivesDataRedis, 8 | getPoolIncentivesUserDataRedis, 9 | setPoolIncentivesUserDataRedis, 10 | } from '../../redis'; 11 | 12 | export const getPoolIncentives = async ( 13 | lendingPoolAddressProvider: string 14 | ): Promise => { 15 | const incentivesKey = `incentives-${lendingPoolAddressProvider}`; 16 | try { 17 | const poolIncentives = await getPoolIncentivesDataRedis(incentivesKey); 18 | if (poolIncentives) { 19 | return poolIncentives.data; 20 | } 21 | } catch (error) { 22 | console.log('Error `getPoolIncentivesDataRedis`', { error, lendingPoolAddressProvider }); 23 | } 24 | 25 | const incentivesData: ReserveIncentivesData[] = await getPoolIncentivesRPC( 26 | lendingPoolAddressProvider 27 | ); 28 | try { 29 | await setPoolIncentivesDataRedis(incentivesKey, { 30 | data: incentivesData, 31 | hash: createHash(incentivesData), 32 | }); 33 | } catch (error) { 34 | console.log('Error `setPoolIncentivesDataRedis`', { error, lendingPoolAddressProvider }); 35 | } 36 | 37 | return incentivesData; 38 | }; 39 | 40 | export const getUserPoolIncentives = async ( 41 | lendingPoolAddressProvider: string, 42 | userAddress: string, 43 | cacheFirst = true 44 | ): Promise => { 45 | const incentivesKey = `incentives-${lendingPoolAddressProvider}`; 46 | if (cacheFirst) { 47 | try { 48 | const userIncentives: UserIncentivesData[] | null = await getPoolIncentivesUserDataRedis( 49 | incentivesKey, 50 | userAddress 51 | ); 52 | if (userIncentives) { 53 | return userIncentives; 54 | } 55 | } catch (error) { 56 | console.log('Error `getPoolIncentivesUserData`', { 57 | error, 58 | lendingPoolAddressProvider, 59 | userAddress, 60 | cacheFirst, 61 | }); 62 | } 63 | } 64 | const userIncentives: UserIncentivesData[] = await getUserPoolIncentivesRPC( 65 | lendingPoolAddressProvider, 66 | userAddress 67 | ); 68 | 69 | try { 70 | await setPoolIncentivesUserDataRedis(incentivesKey, userAddress, userIncentives); 71 | } catch (error) { 72 | console.log('Error `setUserIncentivesDataRedis`', { 73 | error, 74 | lendingPoolAddressProvider, 75 | userAddress, 76 | cacheFirst, 77 | }); 78 | } 79 | 80 | return userIncentives; 81 | }; 82 | -------------------------------------------------------------------------------- /backend/src/services/incentives-data/rpc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReservesIncentiveDataHumanized, 3 | UiIncentiveDataProvider, 4 | UiIncentiveDataProviderContext, 5 | UiIncentiveDataProviderInterface, 6 | UserReservesIncentivesDataHumanized, 7 | } from '@aave/contract-helpers'; 8 | import { ethereumProvider } from '../../helpers/ethereum'; 9 | import { CONFIG, CHAIN_ID } from '../../config'; 10 | import { ReserveIncentivesData } from '../../graphql/object-types/incentives'; 11 | import { UserIncentivesData } from '../../graphql/object-types/user-incentives'; 12 | 13 | let uiIncentiveProvider: UiIncentiveDataProviderInterface; 14 | 15 | export const getPoolIncentivesDataProvider = (): UiIncentiveDataProviderInterface => { 16 | if (!uiIncentiveProvider) { 17 | const uiIncentiveProviderConfig: UiIncentiveDataProviderContext = { 18 | uiIncentiveDataProviderAddress: CONFIG.UI_INCENTIVE_DATA_PROVIDER_ADDRESS, 19 | provider: ethereumProvider, 20 | chainId: CHAIN_ID, 21 | }; 22 | uiIncentiveProvider = new UiIncentiveDataProvider(uiIncentiveProviderConfig); 23 | } 24 | return uiIncentiveProvider; 25 | }; 26 | 27 | /** 28 | * Get the pool reserves incentives data using rpc 29 | * @param lendingPoolAddressProvider The lending pool address provider address 30 | */ 31 | export const getPoolIncentivesRPC = async ( 32 | lendingPoolAddressProvider 33 | ): Promise => { 34 | const uiIncentiveProvider = getPoolIncentivesDataProvider(); 35 | 36 | // TODO: case there for other params? 37 | const rawReservesIncentives: ReservesIncentiveDataHumanized[] = 38 | await uiIncentiveProvider.getReservesIncentivesDataHumanized({ lendingPoolAddressProvider }); 39 | 40 | return rawReservesIncentives; 41 | }; 42 | 43 | export const getUserPoolIncentivesRPC = async ( 44 | lendingPoolAddressProvider: string, 45 | userAddress: string 46 | ): Promise => { 47 | const uiIncentiveProvider = getPoolIncentivesDataProvider(); 48 | 49 | const rawUserReservesIncenvites: UserReservesIncentivesDataHumanized[] = 50 | await uiIncentiveProvider.getUserReservesIncentivesDataHumanized({ 51 | user: userAddress, 52 | lendingPoolAddressProvider, 53 | }); 54 | 55 | return rawUserReservesIncenvites; 56 | }; 57 | -------------------------------------------------------------------------------- /backend/src/services/pool-data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './rpc'; 3 | -------------------------------------------------------------------------------- /backend/src/services/pool-data/provider.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolData } from '../../graphql/object-types/reserve'; 2 | import { UserReservesData } from '../../graphql/object-types/user-reserve'; 3 | import { createHash } from '../../helpers/utils'; 4 | import { 5 | getProtocolDataRedis, 6 | getProtocolUserDataRedis, 7 | setProtocolDataRedis, 8 | setProtocolUserDataRedis, 9 | } from '../../redis'; 10 | import { getProtocolDataRPC, getProtocolUserDataRPC } from './rpc'; 11 | 12 | /** 13 | * Get the protocol data for a pool 14 | * @param lendingPoolAddressProvider The pool address 15 | */ 16 | export async function getProtocolData(lendingPoolAddressProvider: string): Promise { 17 | try { 18 | const protocolData = await getProtocolDataRedis(lendingPoolAddressProvider); 19 | if (protocolData) { 20 | return protocolData.data; 21 | } 22 | } catch (error) { 23 | console.log('Error `getProtocolDataRedis`', { error, lendingPoolAddressProvider }); 24 | } 25 | 26 | const protocolData = await getProtocolDataRPC(lendingPoolAddressProvider); 27 | try { 28 | await setProtocolDataRedis(lendingPoolAddressProvider, { 29 | data: protocolData, 30 | hash: createHash(protocolData), 31 | }); 32 | } catch (error) { 33 | console.log('Error `setProtocolDataRedis`', { error, lendingPoolAddressProvider }); 34 | } 35 | 36 | return protocolData; 37 | } 38 | 39 | /** 40 | * Get the user data 41 | * @param lendingPoolAddressProvider The pool address 42 | * @param userAddress The user address 43 | * @param cacheFirst check the cache first 44 | */ 45 | export async function getProtocolUserData( 46 | lendingPoolAddressProvider: string, 47 | userAddress: string, 48 | cacheFirst = true 49 | ): Promise { 50 | if (cacheFirst) { 51 | try { 52 | const userReserves = await getProtocolUserDataRedis(lendingPoolAddressProvider, userAddress); 53 | if (userReserves) { 54 | return userReserves; 55 | } 56 | } catch (error) { 57 | console.log('Error `getProtocolUserData`', { 58 | error, 59 | lendingPoolAddressProvider, 60 | userAddress, 61 | cacheFirst, 62 | }); 63 | } 64 | } 65 | 66 | const userReserves: UserReservesData = await getProtocolUserDataRPC( 67 | lendingPoolAddressProvider, 68 | userAddress 69 | ); 70 | try { 71 | await setProtocolUserDataRedis(lendingPoolAddressProvider, userAddress, userReserves); 72 | } catch (error) { 73 | console.log('Error `setUserDataRedis`', { 74 | error, 75 | lendingPoolAddressProvider, 76 | userAddress, 77 | cacheFirst, 78 | }); 79 | } 80 | 81 | return userReserves; 82 | } 83 | -------------------------------------------------------------------------------- /backend/src/services/pool-data/rpc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReservesDataHumanized, 3 | UiPoolDataProvider, 4 | UiPoolDataProviderContext, 5 | UiPoolDataProviderInterface, 6 | UserReserveDataHumanized, 7 | } from '@aave/contract-helpers'; 8 | import { CONFIG, CHAIN_ID } from '../../config'; 9 | import { ProtocolData } from '../../graphql/object-types/reserve'; 10 | import { UserReserveData, UserReservesData } from '../../graphql/object-types/user-reserve'; 11 | import { ethereumProvider } from '../../helpers/ethereum'; 12 | 13 | let uiPoolDataProvider: UiPoolDataProviderInterface; 14 | 15 | export const getPoolDataProvider = (): UiPoolDataProviderInterface => { 16 | if (!uiPoolDataProvider) { 17 | const uiPoolDataProviderConfig: UiPoolDataProviderContext = { 18 | uiPoolDataProviderAddress: CONFIG.POOL_UI_DATA_PROVIDER_ADDRESS, 19 | provider: ethereumProvider, 20 | chainId: CHAIN_ID, 21 | }; 22 | uiPoolDataProvider = new UiPoolDataProvider(uiPoolDataProviderConfig); 23 | } 24 | return uiPoolDataProvider; 25 | }; 26 | 27 | /** 28 | * Get the protocol data using rpc 29 | * @param lendingPoolAddressProvider The pool address 30 | */ 31 | export const getProtocolDataRPC = async ( 32 | lendingPoolAddressProvider: string 33 | ): Promise => { 34 | const uiPoolProvider = getPoolDataProvider(); 35 | const { reservesData, baseCurrencyData }: ReservesDataHumanized = 36 | await uiPoolProvider.getReservesHumanized({ lendingPoolAddressProvider }); 37 | 38 | return { 39 | reserves: reservesData, 40 | baseCurrencyData, 41 | }; 42 | }; 43 | 44 | /** 45 | * Get the user data using rpc 46 | * @param lendingPoolAddressProvider The pool address 47 | * @param userAddress The user address 48 | */ 49 | export const getProtocolUserDataRPC = async ( 50 | lendingPoolAddressProvider: string, 51 | userAddress: string 52 | ): Promise => { 53 | const uiPoolProvider = getPoolDataProvider(); 54 | const { 55 | userReserves: userReservesUnfiltered, 56 | userEmodeCategoryId, 57 | }: { userReserves: UserReserveDataHumanized[]; userEmodeCategoryId: number } = 58 | await uiPoolProvider.getUserReservesHumanized({ 59 | lendingPoolAddressProvider, 60 | user: userAddress, 61 | }); 62 | 63 | const userReservesFiltered: UserReserveData[] = userReservesUnfiltered.filter( 64 | (userReserve) => 65 | userReserve.scaledATokenBalance !== '0' || 66 | userReserve.scaledVariableDebt !== '0' || 67 | userReserve.principalStableDebt !== '0' 68 | ); 69 | 70 | return { userReserves: userReservesFiltered, userEmodeCategoryId }; 71 | }; 72 | -------------------------------------------------------------------------------- /backend/src/services/stake-data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './provider'; 2 | export * from './rpc'; 3 | -------------------------------------------------------------------------------- /backend/src/services/stake-data/provider.ts: -------------------------------------------------------------------------------- 1 | import { StakeGeneralUIData } from '../../graphql/object-types/stake-general-data'; 2 | import { StakeUserUIData } from '../../graphql/object-types/stake-user-data'; 3 | import { 4 | getStakeGeneralUIDataRedis, 5 | getStakeUserUIDataRedis, 6 | setStakeGeneralUIDataRedis, 7 | setStakeUserUIDataRedis, 8 | } from '../../redis'; 9 | import { getGeneralStakeUIDataRPC, getUserStakeUIDataRPC } from './rpc'; 10 | 11 | /** 12 | * Get the stake user data 13 | * @param userAddress The user address 14 | * @param forceCache If you should force the cache 15 | */ 16 | export async function getStakeUserUIData( 17 | userAddress: string, 18 | forceCache = false 19 | ): Promise { 20 | if (!forceCache) { 21 | try { 22 | const stakeUserUIData = await getStakeUserUIDataRedis(userAddress); 23 | if (stakeUserUIData) { 24 | return stakeUserUIData; 25 | } 26 | } catch (error) { 27 | console.log('Error `getStakeUserUIDataRedis`', { error, userAddress }); 28 | } 29 | } 30 | 31 | const stakeUserUIData = await getUserStakeUIDataRPC(userAddress); 32 | try { 33 | await setStakeUserUIDataRedis(userAddress, stakeUserUIData); 34 | } catch (error) { 35 | console.log('Error `setStakeUserUIDataRedis`', { error, userAddress }); 36 | } 37 | return stakeUserUIData; 38 | } 39 | 40 | /** 41 | * Get the stake general data 42 | * @param forceCache If you should force the cache 43 | */ 44 | export async function getStakeGeneralUIData(forceCache = false): Promise { 45 | if (!forceCache) { 46 | try { 47 | const stakeGeneralUIData = await getStakeGeneralUIDataRedis(); 48 | if (stakeGeneralUIData) { 49 | return stakeGeneralUIData; 50 | } 51 | } catch (error) { 52 | console.log('Error `getStakeGeneralUIDataRedis`', { error }); 53 | } 54 | } 55 | 56 | const stakeGeneralUIData = await getGeneralStakeUIDataRPC(); 57 | try { 58 | await setStakeGeneralUIDataRedis(stakeGeneralUIData); 59 | } catch (error) { 60 | console.log('Error `setStakeGeneralUIDataRedis`', { error }); 61 | } 62 | return stakeGeneralUIData; 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/services/stake-data/rpc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UiStakeDataProvider, 3 | UiStakeDataProviderContext, 4 | UiStakeDataProviderInterface, 5 | } from '@aave/contract-helpers'; 6 | import { CONFIG, STAKING_CONFIG } from '../../config'; 7 | import { StakeGeneralUIData } from '../../graphql/object-types/stake-general-data'; 8 | import { StakeUserUIData } from '../../graphql/object-types/stake-user-data'; 9 | import { ethereumProvider } from '../../helpers/ethereum'; 10 | 11 | let uiStakeDataProvider: UiStakeDataProviderInterface | null = null; 12 | 13 | export const getStakeDataProvider = (): UiStakeDataProviderInterface | null => { 14 | if (!uiStakeDataProvider && STAKING_CONFIG.STAKE_DATA_PROVIDER) { 15 | const uiStakeDataProviderConfig: UiStakeDataProviderContext = { 16 | uiStakeDataProvider: STAKING_CONFIG.STAKE_DATA_PROVIDER, 17 | provider: ethereumProvider, 18 | }; 19 | 20 | uiStakeDataProvider = new UiStakeDataProvider(uiStakeDataProviderConfig); 21 | } 22 | 23 | return uiStakeDataProvider; 24 | }; 25 | 26 | /** 27 | * Get the stake user data using rpc 28 | * @param userAddress The user address 29 | */ 30 | export const getUserStakeUIDataRPC = async (userAddress: string): Promise => { 31 | const stakeProvider = getStakeDataProvider(); 32 | return stakeProvider!.getUserStakeUIDataHumanized({ user: userAddress }); 33 | }; 34 | 35 | /** 36 | * Get the stake general data using rpc 37 | */ 38 | export const getGeneralStakeUIDataRPC = async (): Promise => { 39 | const stakeProvider = getStakeDataProvider(); 40 | return stakeProvider!.getGeneralStakeUIDataHumanized(); 41 | }; 42 | -------------------------------------------------------------------------------- /backend/src/tasks/last-seen-block.state.ts: -------------------------------------------------------------------------------- 1 | let lastSeenBlockNumbers: { key: string; blockNumber: number }[] = []; 2 | 3 | export const add = (key: string, blockNumber: number) => { 4 | lastSeenBlockNumbers = lastSeenBlockNumbers.filter((c) => c.key !== key); 5 | lastSeenBlockNumbers.push({ key, blockNumber }); 6 | }; 7 | 8 | export const update = (key: string, blockNumber: number) => { 9 | const index = lastSeenBlockNumbers.findIndex((lastSeen) => lastSeen.key === key); 10 | if (index > -1) { 11 | lastSeenBlockNumbers[index].blockNumber = blockNumber; 12 | } else { 13 | add(key, blockNumber); 14 | } 15 | }; 16 | 17 | export const get = (key: string) => { 18 | const index = lastSeenBlockNumbers.findIndex((block) => block.key === key); 19 | if (index > -1) { 20 | return lastSeenBlockNumbers[index].blockNumber; 21 | } 22 | return 0; 23 | }; 24 | 25 | export const remove = (key: string) => { 26 | lastSeenBlockNumbers = lastSeenBlockNumbers.filter((c) => c.key !== key); 27 | }; 28 | -------------------------------------------------------------------------------- /backend/src/tasks/task-helpers.ts: -------------------------------------------------------------------------------- 1 | import { STAKING_CONFIG } from '../config'; 2 | import { getBlockNumber } from '../helpers/ethereum'; 3 | import { sleep } from '../helpers/utils'; 4 | import * as lastSeenBlockState from './last-seen-block.state'; 5 | 6 | interface RunTaskContext { 7 | runEvery: number; 8 | startupHandler?: any; 9 | mainHandler: () => void; 10 | runningHandler: () => boolean; 11 | } 12 | 13 | export async function runTask(context: RunTaskContext) { 14 | if (context.startupHandler) { 15 | await context.startupHandler(); 16 | } 17 | 18 | while (true) { 19 | if (!context.runningHandler()) { 20 | console.log('Job has been stopped'); 21 | break; 22 | } 23 | await context.mainHandler(); 24 | await sleep(context.runEvery); 25 | } 26 | } 27 | 28 | export interface BlockContext { 29 | currentBlock: number; 30 | lastSeenBlock: number; 31 | shouldExecute: boolean; 32 | commit: () => void; // to sync the lastSeen and current on the local state 33 | } 34 | 35 | export async function getBlockContext(key: string, useCache = true): Promise { 36 | const currentBlock = await getBlockNumber(useCache); 37 | const lastSeenBlock = lastSeenBlockState.get(key); 38 | if (currentBlock > lastSeenBlock) { 39 | console.log('new block number seen', { 40 | lastSeenBlock, 41 | currentBlock, 42 | date: new Date(), 43 | }); 44 | } 45 | 46 | return { 47 | currentBlock, 48 | lastSeenBlock, 49 | shouldExecute: currentBlock > lastSeenBlock, 50 | commit: () => lastSeenBlockState.update(key, currentBlock), 51 | }; 52 | } 53 | 54 | export function isStakeEnabled(): boolean { 55 | return !!STAKING_CONFIG; 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/tasks/update-block-number/handler.ts: -------------------------------------------------------------------------------- 1 | import { setBlockNumberRedis } from '../../redis'; 2 | import * as lastSeenBlockState from '../last-seen-block.state'; 3 | import { getBlockContext } from '../task-helpers'; 4 | 5 | const STATE_KEY = 'update-block-number'; 6 | let _running = false; 7 | export const running = () => _running; 8 | 9 | export const handler = async () => { 10 | try { 11 | const blockContext = await getBlockContext(STATE_KEY, false); 12 | if (blockContext.shouldExecute) { 13 | await setBlockNumberRedis(blockContext.currentBlock); 14 | blockContext.commit(); 15 | console.log(`Block number in cache set: ${blockContext.currentBlock}`); 16 | } 17 | } catch (e) { 18 | console.error('Get block number task was failed with error:', e); 19 | } 20 | }; 21 | 22 | export const startUp = () => { 23 | _running = true; 24 | console.log('UpdateBlockNumber job started up successfully'); 25 | }; 26 | 27 | export const stopHandler = () => { 28 | lastSeenBlockState.remove(STATE_KEY); 29 | _running = false; 30 | console.log('UpdateBlockNumber job stopped successfully'); 31 | }; 32 | -------------------------------------------------------------------------------- /backend/src/tasks/update-block-number/run.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '../../config'; 2 | import { runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | async function updateBlockNumber(poolingInterval = CONFIG.BLOCK_NUMBER_POOLING_INTERVAL) { 6 | console.log(`UpdateBlockNumber job starting up with poolingInterval ${poolingInterval / 1000}s`); 7 | 8 | await runTask({ 9 | runEvery: poolingInterval, 10 | startupHandler: startUp, 11 | mainHandler: handler, 12 | runningHandler: running, 13 | }); 14 | } 15 | 16 | updateBlockNumber(); 17 | -------------------------------------------------------------------------------- /backend/src/tasks/update-general-reserves-data/handler.ts: -------------------------------------------------------------------------------- 1 | import { getBlockNumber } from '../../helpers/ethereum'; 2 | import { createHash } from '../../helpers/utils'; 3 | import { pushUpdatedReserveDataToSubscriptions } from '../../pubsub'; 4 | import { getProtocolDataRedis, setProtocolDataRedis } from '../../redis'; 5 | import { getProtocolDataRPC } from '../../services/pool-data'; 6 | import * as lastSeenBlockState from '../last-seen-block.state'; 7 | import { getBlockContext } from '../task-helpers'; 8 | 9 | let _running = false; 10 | export const running = () => _running; 11 | 12 | export const handler = async (lendingPoolAddressProvider: string) => { 13 | try { 14 | const blockContext = await getBlockContext(lendingPoolAddressProvider); 15 | if (blockContext.shouldExecute) { 16 | const [newData, redisProtcolPoolData] = await Promise.all([ 17 | getProtocolDataRPC(lendingPoolAddressProvider), 18 | getProtocolDataRedis(lendingPoolAddressProvider), 19 | ]); 20 | const newDataHash = createHash(newData); 21 | if (newDataHash === redisProtcolPoolData?.hash) { 22 | console.log('Data is the same hash move to next block', { 23 | currentBlock: blockContext.currentBlock, 24 | lendingPoolAddressProvider, 25 | lastSeenBlock: blockContext.lastSeenBlock, 26 | date: new Date(), 27 | }); 28 | blockContext.commit(); 29 | return; 30 | } 31 | await Promise.all([ 32 | setProtocolDataRedis(lendingPoolAddressProvider, { data: newData, hash: newDataHash }), 33 | pushUpdatedReserveDataToSubscriptions(lendingPoolAddressProvider, newData), 34 | ]); 35 | blockContext.commit(); 36 | console.log( 37 | `${lendingPoolAddressProvider}: In block ${blockContext.currentBlock} protocol data in redis was updated with hash ${newDataHash}` 38 | ); 39 | } 40 | } catch (e) { 41 | console.error( 42 | `${lendingPoolAddressProvider}: updateGeneralReservesData task was failed with error`, 43 | e 44 | ); 45 | } 46 | }; 47 | 48 | export const startUp = async (poolAddress: string) => { 49 | const lastSeenBlockNumber = (await getBlockNumber()) - 1; 50 | lastSeenBlockState.add(poolAddress, lastSeenBlockNumber); 51 | _running = true; 52 | 53 | console.log(`UpdateBlockNumber job started up successfully for pool address - ${poolAddress}`); 54 | }; 55 | 56 | export const stopHandler = (poolAddress: string) => { 57 | lastSeenBlockState.remove(poolAddress); 58 | _running = false; 59 | console.log('updateGeneralReservesData job stopped successfully'); 60 | }; 61 | -------------------------------------------------------------------------------- /backend/src/tasks/update-general-reserves-data/run.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '../../config'; 2 | import { runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | CONFIG.PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES.forEach((lendingPoolAddressProvider) => 6 | updateGeneralReservesData(lendingPoolAddressProvider) 7 | ); 8 | 9 | async function updateGeneralReservesData( 10 | lendingPoolAddressProvider: string, 11 | poolingInterval = CONFIG.GENERAL_RESERVES_DATA_POOLING_INTERVAL 12 | ) { 13 | console.log( 14 | `updateGeneralReservesData job starting up with poolingInterval ${ 15 | poolingInterval / 1000 16 | }s for pool address ${lendingPoolAddressProvider}` 17 | ); 18 | 19 | await runTask({ 20 | runEvery: poolingInterval, 21 | startupHandler: startUp, 22 | mainHandler: () => handler(lendingPoolAddressProvider), 23 | runningHandler: running, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/tasks/update-reserve-incentives-data/handler.ts: -------------------------------------------------------------------------------- 1 | import { getBlockNumber } from '../../helpers/ethereum'; 2 | import { createHash } from '../../helpers/utils'; 3 | import { pushUpdatedPoolIncentivesDataToSubscriptions } from '../../pubsub'; 4 | import { getPoolIncentivesDataRedis, setPoolIncentivesDataRedis } from '../../redis'; 5 | import { getPoolIncentivesRPC } from '../../services/incentives-data'; 6 | import * as lastSeenBlockState from '../last-seen-block.state'; 7 | import { getBlockContext } from '../task-helpers'; 8 | 9 | let _running = false; 10 | export const running = () => _running; 11 | 12 | export const handler = async (lendingPoolAddressProvider) => { 13 | try { 14 | const incentivesKey = `incentives-${lendingPoolAddressProvider}`; 15 | const blockContext = await getBlockContext(incentivesKey); 16 | if (blockContext.shouldExecute) { 17 | const [newData, redisPoolIncentivesData] = await Promise.all([ 18 | getPoolIncentivesRPC(lendingPoolAddressProvider), 19 | getPoolIncentivesDataRedis(incentivesKey), 20 | ]); 21 | const newDataHash = createHash(newData); 22 | if (newDataHash === redisPoolIncentivesData?.hash) { 23 | console.log('Data is the same hash move to next block', { 24 | currentBlock: blockContext.currentBlock, 25 | lendingPoolAddressProvider, 26 | lastSeenBlock: blockContext.lastSeenBlock, 27 | date: new Date(), 28 | }); 29 | blockContext.commit(); 30 | return; 31 | } 32 | await Promise.all([ 33 | setPoolIncentivesDataRedis(incentivesKey, { 34 | data: newData, 35 | hash: newDataHash, 36 | }), 37 | pushUpdatedPoolIncentivesDataToSubscriptions(lendingPoolAddressProvider, newData), 38 | ]); 39 | blockContext.commit(); 40 | console.log( 41 | `${lendingPoolAddressProvider}: In block ${blockContext.currentBlock} protocol incentives data in redis was updated with hash ${newDataHash}` 42 | ); 43 | } 44 | } catch (e) { 45 | console.error( 46 | `${lendingPoolAddressProvider}: updateReservesIncentivesData task was failed with error`, 47 | e 48 | ); 49 | } 50 | }; 51 | 52 | export const startUp = async (lendingPoolAddressProvider: string) => { 53 | const lastSeenBlockNumber = (await getBlockNumber()) - 1; 54 | // Added incentives to key, as to have it different from reserve pooling key 55 | lastSeenBlockState.add(`incentives-${lendingPoolAddressProvider}`, lastSeenBlockNumber); 56 | _running = true; 57 | 58 | console.log( 59 | `UpdateBlockNumber job started up successfully for lending pool address provider - incentives-${lendingPoolAddressProvider}` 60 | ); 61 | }; 62 | 63 | export const stopHandler = (lendingPoolAddressProvider: string) => { 64 | lastSeenBlockState.remove(`incentives-${lendingPoolAddressProvider}`); 65 | _running = false; 66 | console.log('updateReservesIncentivesData job stopped successfully'); 67 | }; 68 | -------------------------------------------------------------------------------- /backend/src/tasks/update-reserve-incentives-data/run.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '../../config'; 2 | import { runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | CONFIG.PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES.forEach((lendingPoolAddressProvider) => 6 | updateReservesIncentivesData(lendingPoolAddressProvider) 7 | ); 8 | 9 | async function updateReservesIncentivesData( 10 | lendingPoolAddressProvider: string, 11 | poolingInterval = CONFIG.RESERVE_INCENTIVES_DATA_POOLING_INTERVAL 12 | ) { 13 | console.log( 14 | `updateReservesIncentivesData job starting up with poolingInterval ${ 15 | poolingInterval / 1000 16 | }s for pool address ${lendingPoolAddressProvider}` 17 | ); 18 | 19 | await runTask({ 20 | runEvery: poolingInterval, 21 | startupHandler: () => startUp(lendingPoolAddressProvider), 22 | mainHandler: () => handler(lendingPoolAddressProvider), 23 | runningHandler: running, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/tasks/update-stake-general-ui-data/handler.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from '../../helpers/utils'; 2 | import { pushUpdatedStakeGeneralUIDataToSubscriptions } from '../../pubsub'; 3 | import { setStakeGeneralUIDataRedis } from '../../redis'; 4 | import { getGeneralStakeUIDataRPC } from '../../services/stake-data'; 5 | import * as lastSeenBlockState from '../last-seen-block.state'; 6 | import { getBlockContext } from '../task-helpers'; 7 | import * as lastSeenStoredDataHashState from './last-stored-hash.state'; 8 | 9 | const STATE_KEY = 'update-stake-general'; 10 | let _running = false; 11 | export const running = () => _running; 12 | 13 | export const handler = async () => { 14 | try { 15 | const blockContext = await getBlockContext(STATE_KEY); 16 | if (blockContext.shouldExecute) { 17 | const newData = await getGeneralStakeUIDataRPC(); 18 | const newDataHash = createHash(newData); 19 | if (newDataHash === lastSeenStoredDataHashState.get()) { 20 | // data is identical, go to the next block 21 | console.log('Data is the same hash move to next block', { 22 | currentBlock: blockContext.currentBlock, 23 | lastSeenBlock: blockContext.lastSeenBlock, 24 | date: new Date(), 25 | }); 26 | blockContext.commit(); 27 | return; 28 | } 29 | 30 | lastSeenStoredDataHashState.set(newDataHash); 31 | 32 | setStakeGeneralUIDataRedis(newData); 33 | 34 | await pushUpdatedStakeGeneralUIDataToSubscriptions(); 35 | console.log('published the stake general UI data', new Date()); 36 | 37 | lastSeenBlockState.update(STATE_KEY, blockContext.currentBlock); 38 | } 39 | } catch (e) { 40 | console.error('Update stake user data task failed', e); 41 | } 42 | }; 43 | 44 | export const startUp = () => { 45 | _running = true; 46 | console.log('updateStakeGeneralUIData job started up successfully'); 47 | }; 48 | 49 | export const stopHandler = () => { 50 | lastSeenBlockState.remove(STATE_KEY); 51 | _running = false; 52 | console.log('updateStakeGeneralUIData job stopped successfully'); 53 | }; 54 | -------------------------------------------------------------------------------- /backend/src/tasks/update-stake-general-ui-data/last-stored-hash.state.ts: -------------------------------------------------------------------------------- 1 | // IF `update-stake-general-ui-data` ever needed to run > 1 this needs to turn into an array of state 2 | let lastSeenStoredDataHash = ''; 3 | 4 | export const set = (hash: string) => { 5 | lastSeenStoredDataHash = hash; 6 | }; 7 | 8 | export const get = () => { 9 | return lastSeenStoredDataHash; 10 | }; 11 | -------------------------------------------------------------------------------- /backend/src/tasks/update-stake-general-ui-data/run.ts: -------------------------------------------------------------------------------- 1 | import { STAKING_CONFIG } from '../../config'; 2 | import { isStakeEnabled, runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | export async function updateStakeGeneralUIData(poolingInterval = 1) { 6 | if (isStakeEnabled()) { 7 | console.log( 8 | `updateStakeGeneralUIData job starting up with poolingInterval ${poolingInterval / 1000}s` 9 | ); 10 | 11 | await runTask({ 12 | runEvery: poolingInterval, 13 | startupHandler: startUp, 14 | mainHandler: handler, 15 | runningHandler: running, 16 | }); 17 | } else { 18 | console.log('updateStakeGeneralUIData job not enabled for this network'); 19 | } 20 | } 21 | 22 | STAKING_CONFIG && updateStakeGeneralUIData(STAKING_CONFIG.STAKE_DATA_POOLING_INTERVAL); 23 | -------------------------------------------------------------------------------- /backend/src/tasks/update-stake-user-ui-data/handler.ts: -------------------------------------------------------------------------------- 1 | import { STAKING_CONFIG } from '../../config'; 2 | import { getBlockNumber, getUsersFromLogs } from '../../helpers/ethereum'; 3 | import { pushUpdatedStakeUserUIDataToSubscriptions } from '../../pubsub'; 4 | import * as lastSeenBlockState from '../last-seen-block.state'; 5 | import { getBlockContext } from '../task-helpers'; 6 | 7 | const STATE_KEY = 'update-stake-user-ui-data'; 8 | let _running = false; 9 | export const running = () => _running; 10 | 11 | export const handler = async () => { 12 | try { 13 | const blockContext = await getBlockContext(STATE_KEY); 14 | if (blockContext.shouldExecute) { 15 | const [claimedRewardUsers, transferUsers] = await Promise.all([ 16 | getUsersFromLogs( 17 | [STAKING_CONFIG!.STK_AAVE_TOKEN_ADDRESS, STAKING_CONFIG!.STK_ABPT_TOKEN_ADDRESS], 18 | blockContext.lastSeenBlock, 19 | blockContext.currentBlock, 20 | ['RewardsClaimed(address,address,uint256)'] 21 | ), 22 | getUsersFromLogs( 23 | [ 24 | STAKING_CONFIG!.STK_AAVE_TOKEN_ADDRESS, 25 | STAKING_CONFIG!.STK_ABPT_TOKEN_ADDRESS, 26 | STAKING_CONFIG!.AAVE_TOKEN_ADDRESS, 27 | STAKING_CONFIG!.ABPT_TOKEN, 28 | ], 29 | blockContext.lastSeenBlock, 30 | blockContext.currentBlock, 31 | ['Transfer(address,address,uint256)'] 32 | ), 33 | ]); 34 | 35 | const users = [...new Set([...claimedRewardUsers, ...transferUsers])]; 36 | if (users.length > 0) { 37 | console.log('published the stake user UI data', { users, date: new Date() }); 38 | await Promise.all( 39 | users.map(async (user) => { 40 | await pushUpdatedStakeUserUIDataToSubscriptions(user); 41 | }) 42 | ); 43 | } else { 44 | console.log('no new users affected move to next block', { 45 | lastSeenBlock: blockContext.lastSeenBlock, 46 | currentBlock: blockContext.currentBlock, 47 | date: new Date(), 48 | }); 49 | } 50 | 51 | blockContext.commit(); 52 | } 53 | } catch (e) { 54 | console.error('Update stake user data task failed', e); 55 | } 56 | }; 57 | 58 | export const startUp = async () => { 59 | _running = true; 60 | const blockNumber = (await getBlockNumber()) - 10; 61 | lastSeenBlockState.add(STATE_KEY, blockNumber); 62 | console.log('updateStakeUserUIData job started up successfully'); 63 | }; 64 | 65 | export const stopHandler = () => { 66 | lastSeenBlockState.remove(STATE_KEY); 67 | _running = false; 68 | console.log('updateStakeGeneralUIData job stopped successfully'); 69 | }; 70 | -------------------------------------------------------------------------------- /backend/src/tasks/update-stake-user-ui-data/run.ts: -------------------------------------------------------------------------------- 1 | import { STAKING_CONFIG } from '../../config'; 2 | import { isStakeEnabled, runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | export async function updateStakeUserUIData(poolingInterval = 1) { 6 | if (isStakeEnabled()) { 7 | console.log( 8 | `updateStakeUserUIData job starting up with poolingInterval ${poolingInterval / 1000}s` 9 | ); 10 | 11 | await runTask({ 12 | runEvery: poolingInterval, 13 | startupHandler: startUp, 14 | mainHandler: handler, 15 | runningHandler: running, 16 | }); 17 | } else { 18 | console.log('updateStakeUserUIData job not enabled for this network'); 19 | } 20 | } 21 | 22 | STAKING_CONFIG && updateStakeUserUIData(STAKING_CONFIG.STAKE_DATA_POOLING_INTERVAL); 23 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-data/handler.ts: -------------------------------------------------------------------------------- 1 | import { ILendingPool } from '../../contracts/ethers/ILendingPool'; 2 | import { ethereumProvider, getBlockNumber, getUsersFromLogs } from '../../helpers/ethereum'; 3 | import { pushUpdatedUserReserveDataToSubscriptions } from '../../pubsub'; 4 | import * as lastSeenBlockState from '../last-seen-block.state'; 5 | import { getBlockContext } from '../task-helpers'; 6 | import * as poolContractsState from './pool-contracts.state'; 7 | import * as protocolDataReservesState from './protocol-data-reserves.state'; 8 | 9 | let _running = false; 10 | export const running = () => _running; 11 | 12 | const getUsersWithUsageAsCollateralChange = async ( 13 | lendingPoolContract: ILendingPool, 14 | lastSeenBlock: number, 15 | currentBlock: number 16 | ): Promise => { 17 | const [disabledAsCollateralEvents, enabledAsColteralEvents] = await Promise.all([ 18 | await lendingPoolContract.queryFilter( 19 | lendingPoolContract.filters.ReserveUsedAsCollateralDisabled(null, null), 20 | lastSeenBlock, 21 | currentBlock 22 | ), 23 | await lendingPoolContract.queryFilter( 24 | lendingPoolContract.filters.ReserveUsedAsCollateralEnabled(null, null), 25 | lastSeenBlock, 26 | currentBlock 27 | ), 28 | ]); 29 | return [...disabledAsCollateralEvents, ...enabledAsColteralEvents].map( 30 | (event) => event?.args?.user 31 | ); 32 | }; 33 | 34 | export const handler = async (lendingPoolAddressProvider: string) => { 35 | try { 36 | const blockContext = await getBlockContext(lendingPoolAddressProvider); 37 | if (blockContext.shouldExecute) { 38 | const poolContracts = poolContractsState.get(lendingPoolAddressProvider); 39 | // console.log( 40 | // `${poolContracts.lendingPoolContract.address}: parsing transfer events via Alchemy in blocks ${blockContext.lastSeenBlock} - ${blockContext.currentBlock}` 41 | // ); 42 | 43 | const [usersToUpdate, usersWithUsageAsCollateralChange] = await Promise.all([ 44 | getUsersFromLogs( 45 | protocolDataReservesState.get(lendingPoolAddressProvider), 46 | blockContext.lastSeenBlock, 47 | blockContext.currentBlock 48 | ), 49 | getUsersWithUsageAsCollateralChange( 50 | poolContracts.lendingPoolContract, 51 | blockContext.lastSeenBlock, 52 | blockContext.currentBlock 53 | ), 54 | ]); 55 | // console.log('usersToUpdate', usersToUpdate); 56 | // console.log('usersWithUsageAsCollateralChange', usersWithUsageAsCollateralChange); 57 | 58 | // console.log( 59 | // `${lendingPoolAddressProvider}: Events tracked: ${ 60 | // usersToUpdate.length + usersWithUsageAsCollateralChange.length 61 | // }` 62 | // ); 63 | if (usersToUpdate.length || usersWithUsageAsCollateralChange.length) { 64 | const uniqueUsersToUpdate = [ 65 | ...new Set([...usersToUpdate, ...usersWithUsageAsCollateralChange]), 66 | ]; 67 | // console.log(`${lendingPoolAddressProvider}: Users to update ${uniqueUsersToUpdate.length}`); 68 | await Promise.all( 69 | uniqueUsersToUpdate.map( 70 | async (user) => 71 | await pushUpdatedUserReserveDataToSubscriptions(lendingPoolAddressProvider, user) 72 | ) 73 | ); 74 | } else { 75 | console.log('no new users affected move to next block', { 76 | lastSeenBlock: blockContext.lastSeenBlock, 77 | currentBlock: blockContext.currentBlock, 78 | date: new Date(), 79 | }); 80 | } 81 | 82 | blockContext.commit(); 83 | } 84 | } catch (e) { 85 | console.error(`${lendingPoolAddressProvider}: Users data update was failed with error`, e); 86 | } 87 | }; 88 | 89 | export const startUp = async (lendingPoolAddressProvider: string) => { 90 | const lastSeenBlock = (await getBlockNumber()) - 10; 91 | lastSeenBlockState.add(lendingPoolAddressProvider, lastSeenBlock); 92 | 93 | await poolContractsState.init(lendingPoolAddressProvider, ethereumProvider); 94 | 95 | await protocolDataReservesState.fetchAndAdd(lendingPoolAddressProvider); 96 | protocolDataReservesState.watch(lendingPoolAddressProvider); 97 | 98 | _running = true; 99 | console.log('updateUserData job started up successfully'); 100 | }; 101 | 102 | export const stopHandler = (lendingPoolAddressProvider: string) => { 103 | lastSeenBlockState.remove(lendingPoolAddressProvider); 104 | _running = false; 105 | console.log('updateUserData job stopped successfully'); 106 | }; 107 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-data/pool-contracts.state.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { ILendingPool } from '../../contracts/ethers/ILendingPool'; 3 | import { ILendingPoolAddressesProviderFactory } from '../../contracts/ethers/ILendingPoolAddressesProviderFactory'; 4 | import { ILendingPoolFactory } from '../../contracts/ethers/ILendingPoolFactory'; 5 | 6 | interface PoolContracts { 7 | lendingPoolAddressProvider: string; 8 | lendingPoolContract: ILendingPool; 9 | } 10 | 11 | let poolContracts: PoolContracts[] = []; 12 | 13 | export const get = (lendingPoolAddressProvider: string) => { 14 | const index = poolContracts.findIndex( 15 | (pools) => pools.lendingPoolAddressProvider === lendingPoolAddressProvider 16 | ); 17 | if (index > -1) { 18 | return poolContracts[index]; 19 | } 20 | 21 | throw new Error(`Can not find contracts for pool address - ${lendingPoolAddressProvider}`); 22 | }; 23 | 24 | export const init = async ( 25 | lendingPoolAddressProvider: string, 26 | ethereumProvider: ethers.providers.JsonRpcProvider 27 | ) => { 28 | const lendingPoolAddressesProviderContract = ILendingPoolAddressesProviderFactory.connect( 29 | lendingPoolAddressProvider, 30 | ethereumProvider 31 | ); 32 | 33 | const lendingPoolContract = ILendingPoolFactory.connect( 34 | await lendingPoolAddressesProviderContract.getLendingPool(), 35 | ethereumProvider 36 | ); 37 | 38 | add({ lendingPoolAddressProvider, lendingPoolContract }); 39 | }; 40 | 41 | export const add = (context: PoolContracts) => { 42 | poolContracts = poolContracts.filter( 43 | (c) => c.lendingPoolAddressProvider !== context.lendingPoolAddressProvider 44 | ); 45 | poolContracts.push(context); 46 | }; 47 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-data/protocol-data-reserves.state.ts: -------------------------------------------------------------------------------- 1 | import { RESERVES_LIST_VALIDITY_INTERVAL, CONFIG } from '../../config'; 2 | import { sleep } from '../../helpers/utils'; 3 | import { getProtocolData } from '../../services/pool-data'; 4 | 5 | interface ProtocolDataReserve { 6 | lendingPoolAddressProvider: string; 7 | reservesList: string[]; 8 | timeOfReservesListFetch: number; 9 | } 10 | 11 | let protocolDataReserves: ProtocolDataReserve[] = []; 12 | 13 | export const fetchAndAdd = async (lendingPoolAddressProvider: string) => { 14 | const reservesList: string[] = []; 15 | (await getProtocolData(lendingPoolAddressProvider)).reserves.forEach((reserve) => { 16 | reservesList.push( 17 | reserve.aTokenAddress, 18 | reserve.stableDebtTokenAddress, 19 | reserve.variableDebtTokenAddress 20 | ); 21 | }); 22 | 23 | add(lendingPoolAddressProvider, reservesList); 24 | 25 | return reservesList; 26 | }; 27 | 28 | export const watch = async (lendingPoolAddressProvider: string) => { 29 | while (true) { 30 | try { 31 | // console.log('WATCHER - Fetching new reserves lists', lendingPoolAddressProvider); 32 | await fetchAndAdd(lendingPoolAddressProvider); 33 | // console.log('WATCHER - Fetched new reserves lists', { 34 | // lendingPoolAddressProvider, 35 | // reservesList, 36 | // }); 37 | await sleep(RESERVES_LIST_VALIDITY_INTERVAL); 38 | } catch (error) { 39 | console.error( 40 | `${lendingPoolAddressProvider}: Reserves list loading failed with error`, 41 | error 42 | ); 43 | await sleep(CONFIG.GENERAL_RESERVES_DATA_POOLING_INTERVAL); 44 | } 45 | } 46 | }; 47 | 48 | export const get = (lendingPoolAddressProvider: string): string[] => { 49 | const reservesList = protocolDataReserves.find( 50 | (reserve) => reserve.lendingPoolAddressProvider === lendingPoolAddressProvider 51 | )?.reservesList; 52 | 53 | if (!reservesList?.length) { 54 | throw new Error(`reservesList for ${lendingPoolAddressProvider} is empty`); 55 | } 56 | return reservesList; 57 | }; 58 | 59 | export const add = (lendingPoolAddressProvider: string, reservesList: string[]) => { 60 | protocolDataReserves = protocolDataReserves.filter( 61 | (reserve) => reserve.lendingPoolAddressProvider !== lendingPoolAddressProvider 62 | ); 63 | protocolDataReserves.push({ 64 | lendingPoolAddressProvider, 65 | reservesList, 66 | timeOfReservesListFetch: Date.now(), 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-data/run.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '../../config'; 2 | import { runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | CONFIG.PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES.forEach((lendingPoolAddressProvider) => { 6 | updatUsersData(lendingPoolAddressProvider); 7 | }); 8 | 9 | async function updatUsersData( 10 | lendingPoolAddressProvider: string, 11 | poolingInterval = CONFIG.USERS_DATA_POOLING_INTERVAL 12 | ) { 13 | console.log( 14 | `updateUserData job starting up for pool ${lendingPoolAddressProvider} poolingInterval ${ 15 | poolingInterval / 1000 16 | }s` 17 | ); 18 | 19 | await runTask({ 20 | runEvery: poolingInterval, 21 | startupHandler: () => startUp(lendingPoolAddressProvider), 22 | mainHandler: () => handler(lendingPoolAddressProvider), 23 | runningHandler: running, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-incentives-data/handler.ts: -------------------------------------------------------------------------------- 1 | import * as lastSeenBlockState from '../last-seen-block.state'; 2 | import { ethereumProvider, getBlockNumber, getUsersFromLogs } from '../../helpers/ethereum'; 3 | import * as poolContractsState from './pool-contracts.state'; 4 | import * as protocolDataReservesState from './pool-incentives-data.state'; 5 | import { getBlockContext } from '../task-helpers'; 6 | import { pushUpdatedUserPoolIncentivesDataToSubscriptions } from '../../pubsub'; 7 | 8 | let _running = false; 9 | export const running = () => _running; 10 | 11 | export const handler = async (poolAddress: string) => { 12 | try { 13 | const blockContext = await getBlockContext(poolAddress); 14 | if (blockContext.shouldExecute) { 15 | const poolContracts = poolContractsState.get(poolAddress); 16 | console.log( 17 | `${poolContracts.lendingPoolContract.address}: parsing transfer events via Alchemy in blocks ${blockContext.lastSeenBlock} - ${blockContext.currentBlock}` 18 | ); 19 | 20 | const [usersToUpdate, usersWithClaimedRewards] = await Promise.all([ 21 | getUsersFromLogs( 22 | protocolDataReservesState.get(poolAddress), 23 | blockContext.lastSeenBlock, 24 | blockContext.currentBlock 25 | ), 26 | getUsersFromLogs( 27 | poolContracts.incentiveControllers, 28 | blockContext.lastSeenBlock, 29 | blockContext.currentBlock, 30 | ['RewardsClaimed(address,address,address,uint256)'] 31 | ), 32 | ]); 33 | 34 | console.log('usersToUpdate', usersToUpdate); 35 | console.log('usersWithClaimedRewardsLogs', usersWithClaimedRewards); 36 | 37 | console.log( 38 | `${poolAddress}: Events tracked: ${usersToUpdate.length + usersWithClaimedRewards.length}` 39 | ); 40 | if (usersToUpdate.length || usersWithClaimedRewards.length) { 41 | const uniqueUsersToUpdate = [...new Set([...usersToUpdate, ...usersWithClaimedRewards])]; 42 | console.log(`${poolAddress}: Users to update ${uniqueUsersToUpdate.length}`); 43 | await Promise.all( 44 | uniqueUsersToUpdate.map( 45 | async (user) => 46 | await pushUpdatedUserPoolIncentivesDataToSubscriptions(poolAddress, user) 47 | ) 48 | ); 49 | } else { 50 | console.log('no new users affected move to next block', { 51 | lastSeenBlock: blockContext.lastSeenBlock, 52 | currentBlock: blockContext.currentBlock, 53 | date: new Date(), 54 | }); 55 | } 56 | 57 | blockContext.commit(); 58 | } 59 | } catch (e) { 60 | console.error(`${poolAddress}: Users data update was failed with error`, e); 61 | } 62 | }; 63 | 64 | export const startUp = async (lendingPoolAddressProvider: string) => { 65 | const lastSeenBlock = (await getBlockNumber()) - 10; 66 | lastSeenBlockState.add(lendingPoolAddressProvider, lastSeenBlock); 67 | 68 | await poolContractsState.init(lendingPoolAddressProvider, ethereumProvider); 69 | 70 | await protocolDataReservesState.fetchAndAdd(lendingPoolAddressProvider); 71 | protocolDataReservesState.watch(lendingPoolAddressProvider); 72 | 73 | _running = true; 74 | console.log('updateUserData job started up successfully'); 75 | }; 76 | 77 | export const stopHandler = (lendingPoolAddressProvider: string) => { 78 | lastSeenBlockState.remove(lendingPoolAddressProvider); 79 | _running = false; 80 | console.log('updateUserData job stopped successfully'); 81 | }; 82 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-incentives-data/pool-contracts.state.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { ILendingPool } from '../../contracts/ethers/ILendingPool'; 3 | import { ILendingPoolAddressesProviderFactory } from '../../contracts/ethers/ILendingPoolAddressesProviderFactory'; 4 | import { ILendingPoolFactory } from '../../contracts/ethers/ILendingPoolFactory'; 5 | import { ReserveIncentivesData } from '../../graphql/object-types/incentives'; 6 | import { getPoolIncentivesRPC } from '../../services/incentives-data'; 7 | 8 | interface PoolContracts { 9 | lendingPoolAddressProvider: string; 10 | incentiveControllers: string[]; 11 | lendingPoolContract: ILendingPool; 12 | } 13 | 14 | let poolContracts: PoolContracts[] = []; 15 | 16 | export const get = (lendingPoolAddressProvider: string) => { 17 | const index = poolContracts.findIndex( 18 | (pools) => pools.lendingPoolAddressProvider === lendingPoolAddressProvider 19 | ); 20 | if (index > -1) { 21 | return poolContracts[index]; 22 | } 23 | 24 | throw new Error(`Can not find contracts for pool address - ${lendingPoolAddressProvider}`); 25 | }; 26 | 27 | export const init = async ( 28 | lendingPoolAddressProvider: string, 29 | ethereumProvider: ethers.providers.JsonRpcProvider 30 | ) => { 31 | const lendingPoolAddressesProviderContract = ILendingPoolAddressesProviderFactory.connect( 32 | lendingPoolAddressProvider, 33 | ethereumProvider 34 | ); 35 | 36 | const lendingPoolContract = ILendingPoolFactory.connect( 37 | await lendingPoolAddressesProviderContract.getLendingPool(), 38 | ethereumProvider 39 | ); 40 | 41 | // get all incentive providers: 42 | const reserveIncentives: ReserveIncentivesData[] = await getPoolIncentivesRPC( 43 | lendingPoolAddressProvider 44 | ); 45 | const incentiveControllers: string[] = []; 46 | reserveIncentives.forEach((incentive: ReserveIncentivesData) => { 47 | const aIncentiveController = incentive.aIncentiveData.incentiveControllerAddress.toLowerCase(); 48 | const sIncentiveController = incentive.sIncentiveData.incentiveControllerAddress.toLowerCase(); 49 | const vIncentiveController = incentive.vIncentiveData.incentiveControllerAddress.toLowerCase(); 50 | if (incentiveControllers.indexOf(aIncentiveController) === -1) { 51 | incentiveControllers.push(aIncentiveController); 52 | } 53 | if (incentiveControllers.indexOf(sIncentiveController) === -1) { 54 | incentiveControllers.push(sIncentiveController); 55 | } 56 | if (incentiveControllers.indexOf(vIncentiveController) === -1) { 57 | incentiveControllers.push(vIncentiveController); 58 | } 59 | }); 60 | 61 | if (incentiveControllers.length === 0) { 62 | incentiveControllers.push(ethers.constants.AddressZero); 63 | } 64 | 65 | add({ lendingPoolAddressProvider, lendingPoolContract, incentiveControllers }); 66 | }; 67 | 68 | export const add = (context: PoolContracts) => { 69 | poolContracts = poolContracts.filter( 70 | (c) => c.lendingPoolAddressProvider !== context.lendingPoolAddressProvider 71 | ); 72 | poolContracts.push(context); 73 | }; 74 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-incentives-data/pool-incentives-data.state.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG, RESERVES_LIST_VALIDITY_INTERVAL } from '../../config'; 2 | import { ReserveIncentivesData } from '../../graphql/object-types/incentives'; 3 | import { sleep } from '../../helpers/utils'; 4 | import { getPoolIncentives } from '../../services/incentives-data'; 5 | 6 | interface PoolIncentivesData { 7 | poolAddress: string; 8 | reservesList: string[]; 9 | timeOfReservesListFetch: number; 10 | } 11 | 12 | let protocolDataReserves: PoolIncentivesData[] = []; 13 | 14 | export const fetchAndAdd = async (poolAddress: string) => { 15 | const poolIncentives: ReserveIncentivesData[] = await getPoolIncentives(poolAddress); 16 | const tokenAddresses: Set = new Set(); 17 | 18 | poolIncentives.forEach((reserve) => { 19 | reserve.aIncentiveData.rewardsTokenInformation.map((rewardInfo) => { 20 | if (rewardInfo.emissionEndTimestamp !== 0) { 21 | tokenAddresses.add(reserve.aIncentiveData.tokenAddress); 22 | } 23 | }); 24 | reserve.sIncentiveData.rewardsTokenInformation.map((rewardInfo) => { 25 | if (rewardInfo.emissionEndTimestamp !== 0) { 26 | tokenAddresses.add(reserve.sIncentiveData.tokenAddress); 27 | } 28 | }); 29 | reserve.vIncentiveData.rewardsTokenInformation.map((rewardInfo) => { 30 | if (rewardInfo.emissionEndTimestamp !== 0) { 31 | tokenAddresses.add(reserve.vIncentiveData.tokenAddress); 32 | } 33 | }); 34 | }); 35 | 36 | const reservesList: string[] = Array.from(tokenAddresses); 37 | 38 | add(poolAddress, reservesList); 39 | 40 | return reservesList; 41 | }; 42 | 43 | export const watch = async (poolAddress: string) => { 44 | while (true) { 45 | try { 46 | console.log('WATCHER - Fetching new reserves lists', poolAddress); 47 | const reservesList = await fetchAndAdd(poolAddress); 48 | console.log('WATCHER - Fetched new reserves lists', { poolAddress, reservesList }); 49 | await sleep(RESERVES_LIST_VALIDITY_INTERVAL); 50 | } catch (error) { 51 | console.error(`${poolAddress}: Reserves list loading failed with error`, error); 52 | await sleep(CONFIG.GENERAL_RESERVES_DATA_POOLING_INTERVAL); 53 | } 54 | } 55 | }; 56 | 57 | export const get = (poolAddress: string): string[] => { 58 | console.log('pool address: ', poolAddress); 59 | console.log('protocl data reserves::: ', protocolDataReserves); 60 | 61 | const reservesList = protocolDataReserves.find( 62 | (reserve) => reserve.poolAddress === poolAddress 63 | )?.reservesList; 64 | 65 | if (!reservesList?.length) { 66 | return []; 67 | // throw new Error(`reservesList for ${poolAddress} is empty`); 68 | } 69 | return reservesList; 70 | }; 71 | 72 | export const add = (poolAddress: string, reservesList: string[]) => { 73 | protocolDataReserves = protocolDataReserves.filter( 74 | (reserve) => reserve.poolAddress !== poolAddress 75 | ); 76 | protocolDataReserves.push({ poolAddress, reservesList, timeOfReservesListFetch: Date.now() }); 77 | }; 78 | -------------------------------------------------------------------------------- /backend/src/tasks/update-users-incentives-data/run.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '../../config'; 2 | import { runTask } from '../task-helpers'; 3 | import { handler, running, startUp } from './handler'; 4 | 5 | CONFIG.PROTOCOL_ADDRESSES_PROVIDER_ADDRESSES.forEach((lendingPoolAddressProvider) => 6 | updateUsersIncentivesData(lendingPoolAddressProvider) 7 | ); 8 | 9 | async function updateUsersIncentivesData( 10 | lendingPoolAddressProvider: string, 11 | poolingInterval = CONFIG.USER_INCENTIVES_DATA_POOLING_INTERVAL 12 | ) { 13 | console.log( 14 | `updateUserIncentivesData job starting up with poolingInterval ${ 15 | poolingInterval / 1000 16 | }s for pool address ${lendingPoolAddressProvider}` 17 | ); 18 | 19 | await runTask({ 20 | runEvery: poolingInterval, 21 | startupHandler: () => startUp(lendingPoolAddressProvider), 22 | mainHandler: () => handler(lendingPoolAddressProvider), 23 | runningHandler: running, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es6", "es7", "es2018", "es2020", "esnext.asynciterable"], 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "./build", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "sourceMap": true, 13 | "plugins": [ 14 | { 15 | "name": "typescript-tslint-plugin" 16 | } 17 | ] 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | x-common-environment: &common-environment 4 | image: node:16 5 | user: node 6 | working_dir: /app 7 | volumes: 8 | - ./backend:/app 9 | restart: on-failure 10 | environment: 11 | REDIS_HOST: "redis" 12 | CHAIN_ID: "137" 13 | RPC_URL: "https://polygon-rpc.com" 14 | 15 | depends_on: 16 | - redis 17 | 18 | services: 19 | api: 20 | <<: *common-environment 21 | ports: 22 | - 3000:3000 23 | command: "npm run start" 24 | 25 | general_reserves_data_loader: 26 | <<: *common-environment 27 | command: "npm run job:update-general-reserves-data" 28 | 29 | reserves_incentives_data_loader: 30 | <<: *common-environment 31 | command: "npm run job:update-reserve-incentives-data" 32 | 33 | user_incentives_data_loader: 34 | <<: *common-environment 35 | command: "npm run job:update-users-incentives-data" 36 | 37 | user_data_loader: 38 | <<: *common-environment 39 | command: "npm run job:update-users-data" 40 | 41 | stake_user_ui_data_loader: 42 | <<: *common-environment 43 | command: "npm run job:update-stake-user-ui-data" 44 | 45 | stake_general_ui_data_loader: 46 | <<: *common-environment 47 | command: "npm run job:update-stake-general-ui-data" 48 | 49 | block_number_loader: 50 | <<: *common-environment 51 | command: "npm run job:update-block-number" 52 | 53 | redis: 54 | image: redis:6-alpine 55 | -------------------------------------------------------------------------------- /k8s/backend_components.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence, Optional 2 | 3 | from kdsl.apps.v1 import Deployment, DeploymentSpec, DeploymentStrategy, RollingUpdateDeployment 4 | from kdsl.core.v1 import Service, ServiceSpec, PodSpec, ObjectMeta, ContainerItem, Probe, ExecAction, HTTPGetAction 5 | from kdsl.extra import mk_env 6 | from kdsl.recipe import choice, collection 7 | 8 | import values 9 | 10 | env = mk_env( 11 | REDIS_HOST="redis", 12 | RPC_URL=choice( 13 | chain1=values.MAINNET_RPC, 14 | chain137=values.POLYGON_RPC, 15 | chain43114="https://api.avax.network/ext/bc/C/rpc" 16 | ), 17 | CHAIN_ID=values.CHAIN_ID, 18 | ) 19 | 20 | api_probe = Probe( 21 | httpGet=HTTPGetAction( 22 | port="http", 23 | path='/.well-known/apollo/server-health', 24 | scheme='HTTP' 25 | ), 26 | initialDelaySeconds=5, 27 | periodSeconds=10, 28 | timeoutSeconds=3, 29 | failureThreshold=5, 30 | ) 31 | 32 | worker_probe = Probe( 33 | exec=ExecAction( 34 | command="ps -p 1".split() 35 | ), 36 | initialDelaySeconds=5, 37 | periodSeconds=20, 38 | failureThreshold=5, 39 | ) 40 | 41 | 42 | def mk_backend_entries( 43 | name: str, 44 | command: Sequence[str], 45 | probe: Probe = worker_probe, 46 | port: Optional[int] = None, 47 | scale: int = 1, 48 | ): 49 | labels = dict(component=name) 50 | 51 | metadata = ObjectMeta( 52 | name=name, 53 | namespace=values.NAMESPACE, 54 | labels=dict(**labels, **values.shared_labels, **values.datadog_labels(name)), 55 | annotations=values.shared_annotations 56 | ) 57 | 58 | if port is not None: 59 | service = Service( 60 | metadata=metadata, 61 | spec=ServiceSpec( 62 | selector=labels, 63 | ports={ 64 | port: dict(name="http"), 65 | }, 66 | ), 67 | ) 68 | service_list = [service] 69 | container_ports_mixin = dict( 70 | ports={ 71 | port: dict(name="http", protocol="TCP"), 72 | } 73 | ) 74 | else: 75 | service_list = [] 76 | container_ports_mixin = dict() 77 | 78 | pod_spec = PodSpec( 79 | containers={ 80 | name: ContainerItem( 81 | image=values.IMAGE, 82 | imagePullPolicy="Always", 83 | **container_ports_mixin, 84 | command=command, 85 | env=env, 86 | readinessProbe=probe, 87 | livenessProbe=probe, 88 | ), 89 | }, 90 | ) 91 | 92 | deployment = Deployment( 93 | metadata=metadata, 94 | spec=DeploymentSpec( 95 | replicas=scale, 96 | selector=dict(matchLabels=labels), 97 | progressDeadlineSeconds=180, 98 | strategy=DeploymentStrategy( 99 | type="RollingUpdate", 100 | rollingUpdate=RollingUpdateDeployment( 101 | maxUnavailable=1, 102 | maxSurge=1, 103 | ), 104 | ), 105 | template=dict( 106 | metadata=ObjectMeta( 107 | labels=dict(**metadata.labels), 108 | annotations=values.shared_annotations 109 | ), 110 | spec=pod_spec, 111 | ), 112 | ), 113 | ) 114 | 115 | return [*service_list, deployment] 116 | 117 | 118 | entries = collection( 119 | base=[ 120 | *mk_backend_entries( 121 | name="api", 122 | command=["npm", "run", "prod"], 123 | probe=api_probe, 124 | port=3000, 125 | scale=1 126 | ), 127 | *mk_backend_entries( 128 | name="protocol-data-loader", 129 | command=["npm", "run", "job:update-general-reserves-data"], 130 | ), 131 | *mk_backend_entries( 132 | name="reserve-incentives", 133 | command=["npm", "run", "job:update-reserve-incentives-data"], 134 | ), 135 | *mk_backend_entries( 136 | name="user-incentives", 137 | command=["npm", "run", "job:update-users-incentives-data"], 138 | ), 139 | *mk_backend_entries( 140 | name="user-data-loader", 141 | command=["npm", "run", "job:update-users-data"], 142 | ), 143 | *mk_backend_entries( 144 | name="update-block-number-loader", 145 | command=["npm", "run", "job:update-block-number"], 146 | ), 147 | ], 148 | chain1=[ 149 | *mk_backend_entries( 150 | name="stake-general-data-loader", 151 | command=["npm", "run", "job:update-stake-general-ui-data"], 152 | ), 153 | *mk_backend_entries( 154 | name="stake-user-data-loader", 155 | command=["npm", "run", "job:update-stake-user-ui-data"], 156 | ), 157 | ], 158 | ) 159 | -------------------------------------------------------------------------------- /k8s/common.py: -------------------------------------------------------------------------------- 1 | from kdsl.core.v1 import Namespace, ObjectMeta 2 | 3 | import values 4 | 5 | namespace = Namespace( 6 | metadata=ObjectMeta( 7 | name=values.NAMESPACE, 8 | labels=values.shared_labels, 9 | annotations=values.shared_annotations 10 | ) 11 | ) 12 | 13 | entries = [namespace] 14 | -------------------------------------------------------------------------------- /k8s/ingress.py: -------------------------------------------------------------------------------- 1 | from kdsl.core.v1 import ObjectMeta 2 | from kdsl.networking.v1beta1 import Ingress, IngressSpec, IngressTLS, IngressRule, IngressBackend, HTTPIngressRuleValue, \ 3 | HTTPIngressPath 4 | 5 | import values 6 | 7 | ingress = Ingress( 8 | metadata=ObjectMeta( 9 | name='main', 10 | namespace=values.NAMESPACE, 11 | labels=values.shared_labels, 12 | annotations={ 13 | "nginx.ingress.kubernetes.io/auth-tls-secret": "default/cf-mtls", 14 | "nginx.ingress.kubernetes.io/auth-tls-verify-client": "on", 15 | "nginx.ingress.kubernetes.io/auth-tls-verify-depth": "1", 16 | **values.shared_annotations 17 | } 18 | ), 19 | spec=IngressSpec( 20 | rules=[IngressRule( 21 | host=values.DOMAIN, 22 | http=HTTPIngressRuleValue( 23 | paths=[HTTPIngressPath( 24 | backend=IngressBackend( 25 | serviceName="api", 26 | servicePort=3000 27 | ) 28 | )] 29 | ) 30 | )], 31 | tls=[IngressTLS( 32 | hosts=[values.DOMAIN] 33 | )] 34 | ) 35 | ) 36 | 37 | entries = [ingress] 38 | -------------------------------------------------------------------------------- /k8s/main.py: -------------------------------------------------------------------------------- 1 | from kdsl.utils import render_to_stdout 2 | 3 | import backend_components 4 | import common 5 | import ingress 6 | import redis_component 7 | 8 | entries = [ 9 | *common.entries, 10 | *backend_components.entries, 11 | *redis_component.entries, 12 | *ingress.entries, 13 | ] 14 | 15 | if __name__ == "__main__": 16 | render_to_stdout(entries) 17 | -------------------------------------------------------------------------------- /k8s/redis_component.py: -------------------------------------------------------------------------------- 1 | from kdsl.apps.v1 import Deployment, DeploymentSpec 2 | from kdsl.core.v1 import Service, ServiceSpec, PodSpec, ObjectMeta, ContainerItem 3 | 4 | import values 5 | 6 | name = "redis" 7 | labels = dict(component=name) 8 | annotations = values.shared_annotations 9 | 10 | 11 | metadata = ObjectMeta( 12 | name=name, 13 | namespace=values.NAMESPACE, 14 | labels=dict(**labels, **values.shared_labels, **values.datadog_labels(name)), 15 | annotations=values.shared_annotations 16 | ) 17 | 18 | 19 | service = Service( 20 | metadata=metadata, 21 | spec=ServiceSpec( 22 | selector=labels, 23 | ports={ 24 | 6379: dict(name="redis"), 25 | }, 26 | ), 27 | ) 28 | 29 | 30 | pod_spec = PodSpec( 31 | containers=dict( 32 | redis=ContainerItem( 33 | image="redis:6-alpine", 34 | imagePullPolicy="Always", 35 | ports={ 36 | 6379: dict(name="redis", protocol="TCP"), 37 | }, 38 | ), 39 | ), 40 | ) 41 | 42 | 43 | deployment = Deployment( 44 | metadata=metadata, 45 | spec=DeploymentSpec( 46 | replicas=1, 47 | selector=dict(matchLabels=labels), 48 | template=dict( 49 | metadata=ObjectMeta( 50 | labels=dict(**metadata.labels), 51 | annotations=annotations 52 | ), 53 | spec=pod_spec, 54 | ), 55 | ), 56 | ) 57 | 58 | 59 | entries = [service, deployment] 60 | -------------------------------------------------------------------------------- /k8s/render.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | RENDER_FILE=${1:-"main.py"} 5 | CHAIN_ID=${2:-1} 6 | 7 | docker run -it --rm -v "$(pwd)":/app --workdir=/app \ 8 | -e NAMESPACE="cache-${CHAIN_ID}" \ 9 | -e IMAGE='ghcr.io/aave/aave-ui-caching-server:9610077d06eecb7b25b59655af7d1b3ff8e81725' \ 10 | -e DOMAIN='example.com' \ 11 | -e RECIPE="chain${CHAIN_ID}" \ 12 | -e CHAIN_ID="${CHAIN_ID}" \ 13 | -e POLYGON_RPC="poly-secret-rpc" \ 14 | -e MAINNET_RPC="main-secret-rpc" \ 15 | -e COMMIT_SHA="$(git rev-parse --verify HEAD)" \ 16 | -e ENV_NAME="rendered" \ 17 | qwolphin/kdsl:1.21.8 \ 18 | python3 "${RENDER_FILE}" 19 | -------------------------------------------------------------------------------- /k8s/values.py: -------------------------------------------------------------------------------- 1 | from envparse import env # type: ignore 2 | 3 | NAMESPACE: str = env.str("NAMESPACE") 4 | IMAGE: str = env.str("IMAGE") 5 | DOMAIN: str = env.str("DOMAIN") 6 | 7 | CHAIN_ID: str = env.str("CHAIN_ID") 8 | POLYGON_RPC: str = env.str("POLYGON_RPC") 9 | MAINNET_RPC: str = env.str("MAINNET_RPC") 10 | 11 | shared_labels = dict( 12 | project="aave", 13 | app="caching-server", 14 | commit_sha=env.str("COMMIT_SHA"), 15 | environment=env.str("ENV_NAME"), 16 | ) 17 | 18 | 19 | def datadog_labels(service: str): 20 | return { 21 | "tags.datadoghq.com/env": env.str("ENV_NAME"), 22 | "tags.datadoghq.com/service": service, 23 | "tags.datadoghq.com/version": env.str("COMMIT_SHA"), 24 | } 25 | 26 | 27 | shared_annotations = dict( 28 | git_repo="https://github.com/aave/aave-ui-caching-server" 29 | ) 30 | --------------------------------------------------------------------------------