├── .dockerignore ├── .env.example ├── .eslintrc.cjs ├── .github └── workflows │ ├── check-pr.yml │ ├── deploy-branch.yml │ ├── deploy-do-indexer.yml │ ├── deploy-do-web.yml │ └── deploy-web-server.yml ├── .gitignore ├── .prettierrc.json ├── .tool-versions ├── Dockerfile ├── Dockerfile.index ├── Dockerfile.web ├── README.md ├── READ_DOCKER_KB.md ├── docker-compose.yml ├── docs ├── caching.md ├── deploy-to-fly.md ├── devops.md ├── donations.md ├── reindexing.md └── write-lock.md ├── ecosystem.config.cjs ├── fly.log-shipper.toml ├── fly.staging.toml ├── fly.toml ├── indexer-compose.yml ├── kb-deployment.yaml ├── kb-service.yaml ├── lefthook.yml ├── package-lock.json ├── package.json ├── src ├── address.ts ├── blockCache.ts ├── calculator │ ├── calculateMatches.ts │ ├── calculateMatchingEstimates.ts │ ├── calculationConfig.ts │ ├── coefficientOverrides.ts │ ├── dataProvider │ │ ├── cachedDataProvider.ts │ │ ├── databaseDataProvider.ts │ │ ├── fileSystemDataProvider.ts │ │ └── index.ts │ ├── errors.ts │ ├── linearQf │ │ ├── index.ts │ │ └── worker.ts │ ├── options.ts │ ├── roundContributionsCache.ts │ └── votes.ts ├── config.ts ├── contractSubscriptionPruner.ts ├── database │ ├── changeset.ts │ ├── index.ts │ ├── migrate.ts │ └── schema.ts ├── deprecatedJsonDatabase.ts ├── diskCache.ts ├── global.d.ts ├── http │ ├── api │ │ ├── clientError.ts │ │ └── v1 │ │ │ ├── exports.ts │ │ │ ├── index.ts │ │ │ ├── matches.ts │ │ │ └── status.ts │ └── app.ts ├── index.ts ├── indexer │ ├── abis │ │ ├── allo-v1 │ │ │ ├── v1 │ │ │ │ ├── ProgramFactory.ts │ │ │ │ ├── ProgramImplementation.ts │ │ │ │ ├── ProjectRegistry.ts │ │ │ │ ├── QuadraticFundingVotingStrategyFactory.ts │ │ │ │ ├── QuadraticFundingVotingStrategyImplementation.ts │ │ │ │ ├── RoundFactory.ts │ │ │ │ └── RoundImplementation.ts │ │ │ └── v2 │ │ │ │ ├── DirectPayoutStrategyFactory.ts │ │ │ │ ├── DirectPayoutStrategyImplementation.ts │ │ │ │ ├── MerklePayoutStrategyFactory.ts │ │ │ │ ├── MerklePayoutStrategyImplementation.ts │ │ │ │ ├── ProjectRegistry.ts │ │ │ │ ├── QuadraticFundingVotingStrategyFactory.ts │ │ │ │ ├── QuadraticFundingVotingStrategyImplementation.ts │ │ │ │ ├── RoundFactory.ts │ │ │ │ └── RoundImplementation.ts │ │ ├── allo-v2 │ │ │ ├── AlloV1ToV2ProfileMigration.ts │ │ │ ├── Registry.ts │ │ │ └── v1 │ │ │ │ ├── Allo.ts │ │ │ │ ├── DirectAllocationStrategy.ts │ │ │ │ ├── DirectGrantsLiteStrategy.ts │ │ │ │ ├── DirectGrantsSimpleStrategy.ts │ │ │ │ ├── DonationVotingMerkleDistributionDirectTransferStrategy.ts │ │ │ │ ├── EasyRPGFStrategy.ts │ │ │ │ ├── EasyRetroFundingStrategy.ts │ │ │ │ ├── IStrategy.ts │ │ │ │ └── Registry.ts │ │ ├── gitcoin-attestation-network │ │ │ └── GitcoinGrantsResolver.ts │ │ └── index.ts │ ├── allo │ │ ├── application.ts │ │ ├── roundMetadata.ts │ │ ├── v1 │ │ │ ├── applicationMetaPtrUpdated.ts │ │ │ ├── handleEvent.test.ts │ │ │ ├── handleEvent.ts │ │ │ ├── matchAmountUpdated.ts │ │ │ ├── roleGranted.ts │ │ │ ├── roleRevoked.ts │ │ │ ├── roles.ts │ │ │ ├── roundMetaPtrUpdated.ts │ │ │ └── timeUpdated.ts │ │ └── v2 │ │ │ ├── handleEvent.test.ts │ │ │ ├── handleEvent.ts │ │ │ ├── parsePoolMetadata.ts │ │ │ ├── poolMetadata.ts │ │ │ ├── roleGranted.ts │ │ │ ├── roleRevoked.ts │ │ │ └── strategy.ts │ ├── applicationMetadata.ts │ ├── gitcoin-attestation-network │ │ └── handleEvent.ts │ ├── indexer.ts │ ├── projectMetadata.ts │ └── types.ts ├── passport │ ├── index.test.fixtures.ts │ ├── index.test.ts │ └── index.ts ├── prices │ ├── coinGecko.ts │ ├── common.ts │ └── provider.ts ├── resourceMonitor.test.ts ├── resourceMonitor.ts ├── test │ ├── calculator │ │ ├── proportionalMatch.test.fixtures.ts │ │ ├── proportionalMatch.test.ts │ │ └── votes.test.ts │ ├── fixtures │ │ ├── applications.json │ │ ├── overrides-with-floating-coefficient.csv │ │ ├── overrides-with-invalid-coefficient.csv │ │ ├── overrides-without-coefficient.csv │ │ ├── overrides-without-transaction-id.csv │ │ ├── overrides.csv │ │ ├── passport_scores.json │ │ ├── rounds.json │ │ ├── votes-with-bad-recipient.json │ │ └── votes.json │ ├── http │ │ └── app.test.ts │ └── utils.ts ├── tokenMath.test.ts ├── tokenMath.ts ├── types.ts └── utils │ └── index.ts ├── tsconfig.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | /data 2 | /node_modules 3 | /.cache 4 | /.git 5 | /dist 6 | .env 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INDEXED_CHAINS=sepolia 2 | PASSPORT_SCORER_ID=335 3 | STORAGE_DIR=.var 4 | 5 | DEPLOYMENT_ENVIRONMENT=local 6 | PORT=8080 7 | LOG_LEVEL=debug 8 | # The BUILD_TAG will be set at deployment time to the latest commit hash 9 | BUILD_TAG=local 10 | ENABLE_RESOURCE_MONITOR=false 11 | 12 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/grants_stack_indexer 13 | 14 | DD_API_KEY=123 15 | DD_SITE=datadoghq.eu 16 | DD_ENV=development 17 | 18 | #ESTIMATES_LINEARQF_WORKER_POOL_SIZE=10 19 | 20 | #COINGECKO_API_KEY= 21 | 22 | #LOG_LEVEL= 23 | #BUILD_TAG= 24 | #PINO_PRETTY=true 25 | #MAINNET_RPC_URL= 26 | #GOERLI_RPC_URL= 27 | #SEPOLIA_RPC_URL= 28 | #OPTIMISM_RPC_URL= 29 | #FANTOM_RPC_URL= 30 | #ZKSYNC_RPC_URL= 31 | #ZKSYNC_TESTNET_RPC_URL= 32 | #PGN_TESTNET_RPC_URL= 33 | #PGN_RPC_URL= 34 | #ARBITRUM_RPC_URL= 35 | #ARBITRUM_GOERLI_RPC_URL= 36 | #AVALANCHE_RPC_URL= 37 | #POLYGON_RPC_URL= 38 | #POLYGON_MUMBAI_RPC_URL= 39 | #SCROLL_SEPOLIA_RPC_URL= 40 | #SEI_MAINNET_RPC_URL= 41 | #SEI_DEVNET_RPC_URL= 42 | #CELO_MAINET_RPC_URL= 43 | #CELO_TESTNET_RPC_URL= 44 | #LUKSO_MAINNET_RPC_URL= 45 | #LUKSO_TESTNET_RPC_URL 46 | #METIS_ANDROMEDA_RPC_URL 47 | #GNOSIS_RPC_URL 48 | #HEDERA_RPC_URL 49 | 50 | #COINGECKO_API_KEY= 51 | #IPFS_GATEWAYs=[] 52 | #WHITELISTED_ADDRESSES=["0x123..","0x456.."] 53 | 54 | # optional, enable the Postgraphile Pro plugin: https://www.npmjs.com/package/@graphile/pro 55 | #GRAPHILE_LICENSE 56 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 5 | ], 6 | ignorePatterns: ["/*", "!/src"], 7 | parserOptions: { 8 | project: `./tsconfig.json`, 9 | }, 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["@typescript-eslint", "no-only-tests"], 12 | root: true, 13 | rules: { 14 | "no-unused-vars": "off", 15 | "no-only-tests/no-only-tests": "error", 16 | "@typescript-eslint/no-misused-promises": [ 17 | "error", 18 | { 19 | checksVoidReturn: { 20 | arguments: false, 21 | }, 22 | }, 23 | ], 24 | "@typescript-eslint/no-unused-vars": [ 25 | "error", 26 | { 27 | argsIgnorePattern: "^_", 28 | varsIgnorePattern: "^_", 29 | caughtErrorsIgnorePattern: "^_", 30 | }, 31 | ], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.github/workflows/check-pr.yml: -------------------------------------------------------------------------------- 1 | name: Check Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Set up fly 15 | uses: superfly/flyctl-actions/setup-flyctl@master 16 | 17 | # (ab)use the development environment until we build on github 18 | - name: Build and test 19 | run: | 20 | flyctl -c fly.staging.toml deploy --remote-only --build-only 21 | env: 22 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_STAGE }} 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy-branch.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Branch 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up fly 20 | uses: superfly/flyctl-actions/setup-flyctl@master 21 | 22 | - name: Set variables for production 23 | run: | 24 | echo "FLY_CONFIG=fly.toml" >> $GITHUB_ENV 25 | echo "GIT_SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV 26 | echo "FLY_APP_NAME=indexer-v2" >> $GITHUB_ENV 27 | if: ${{ github.ref == 'refs/heads/main' }} 28 | 29 | - name: Build and test 30 | run: | 31 | flyctl -c ${{ env.FLY_CONFIG }} deploy --remote-only --build-only --push --image-label deployment-$GIT_SHA_SHORT 32 | env: 33 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PRODUCTION }} 34 | 35 | - name: Deploy Indexer 36 | run: | 37 | flyctl -c ${{ env.FLY_CONFIG }} deploy --wait-timeout=7200 --env BUILD_TAG=`git rev-parse --short HEAD` --image registry.fly.io/$FLY_APP_NAME:deployment-$GIT_SHA_SHORT --process-groups=indexer 38 | env: 39 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PRODUCTION }} 40 | 41 | - name: Deploy HTTP 42 | run: | 43 | flyctl -c ${{ env.FLY_CONFIG }} deploy --wait-timeout=7200 --env BUILD_TAG=`git rev-parse --short HEAD` --image registry.fly.io/$FLY_APP_NAME:deployment-$GIT_SHA_SHORT --process-groups=web 44 | env: 45 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PRODUCTION }} 46 | 47 | - name: Smoke test 48 | run: | 49 | curl --silent --show-error --fail-with-body https://${{ env.FLY_APP_NAME }}.fly.dev/api/v1/status 50 | -------------------------------------------------------------------------------- /.github/workflows/deploy-do-indexer.yml: -------------------------------------------------------------------------------- 1 | name: Deploy DO Indexer 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: 20 14 | - name: Install dependencies 15 | run: npm ci 16 | - name: Test build 17 | run: npm run build 18 | deploy-indexer: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Deploy to production 24 | uses: appleboy/ssh-action@master 25 | with: 26 | host: ${{ secrets.DO_INDEXER_IP }} 27 | username: ${{ secrets.DO_USER }} 28 | key: ${{ secrets.DO_SSH_KEY }} 29 | script: | 30 | export N_PREFIX="$HOME/n"; [[ :$PATH: == *":$N_PREFIX/bin:"* ]] || PATH+=":$N_PREFIX/bin" 31 | cd grants-stack-indexer 32 | git fetch origin main 33 | git reset --hard origin/main 34 | npm install && npm run build 35 | pm2 reload indexer 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy-do-web.yml: -------------------------------------------------------------------------------- 1 | name: Deploy DO Web 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FILENAME: Dockerfile.web 7 | IMAGE_NAME: gitcoinco/indexer-web 8 | IMAGE_TAG: ${{ github.sha }} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Build the Docker image 18 | run: docker build -f "$FILENAME" -t "$IMAGE_NAME:$IMAGE_TAG" . # build the Docker image using envs defined above 19 | 20 | # login to dockerhub then push the image to the dockerhub repo 21 | - name: Push Docker image 22 | run: |- 23 | echo ${{secrets.DOCKERHUB_PASS}} | docker login -u ${{secrets.DOCKERHUB_USER}} --password-stdin 24 | docker push "$IMAGE_NAME:$IMAGE_TAG" 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy-web-server.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Web Server 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up fly 18 | uses: superfly/flyctl-actions/setup-flyctl@master 19 | 20 | - name: Set variables for production 21 | run: | 22 | echo "FLY_CONFIG=fly.toml" >> $GITHUB_ENV 23 | echo "GIT_SHA_SHORT=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV 24 | echo "FLY_APP_NAME=indexer-v2" >> $GITHUB_ENV 25 | if: ${{ github.ref == 'refs/heads/main' }} 26 | 27 | - name: Build and test 28 | run: | 29 | flyctl -c ${{ env.FLY_CONFIG }} deploy --remote-only --build-only --push --image-label deployment-$GIT_SHA_SHORT 30 | env: 31 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PRODUCTION }} 32 | 33 | - name: Deploy HTTP 34 | run: | 35 | flyctl -c ${{ env.FLY_CONFIG }} deploy --wait-timeout=7200 --env BUILD_TAG=`git rev-parse --short HEAD` --image registry.fly.io/$FLY_APP_NAME:deployment-$GIT_SHA_SHORT --process-groups=web 36 | env: 37 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PRODUCTION }} 38 | 39 | - name: Smoke test 40 | run: | 41 | curl --silent --show-error --fail-with-body https://${{ env.FLY_APP_NAME }}.fly.dev/api/v1/status 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | .env 3 | .cache 4 | dist 5 | node_modules 6 | .idea 7 | /test/snapshot.* 8 | schema.graphql 9 | 10 | # variable data file directory 11 | /.var 12 | 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.12.2 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | COPY package*.json ./ 7 | 8 | RUN npm ci 9 | 10 | # Bundle app source 11 | COPY src src 12 | COPY tsconfig.json ./ 13 | COPY .eslintrc.cjs ./ 14 | COPY .prettierrc.json ./ 15 | 16 | RUN npm run build 17 | RUN npm run lint 18 | 19 | EXPOSE 8080 20 | 21 | RUN npm run test 22 | 23 | CMD [ "npm", "start" ] 24 | -------------------------------------------------------------------------------- /Dockerfile.index: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Bundle app source 6 | COPY src src 7 | COPY tsconfig.json ./ 8 | COPY vite.config.ts ./ 9 | COPY .eslintrc.cjs ./ 10 | COPY .prettierrc.json ./ 11 | COPY package*.json ./ 12 | 13 | RUN npm ci 14 | RUN npm run lint 15 | 16 | RUN npm run build 17 | 18 | RUN npm run test 19 | 20 | 21 | EXPOSE 8080 22 | 23 | 24 | CMD [ "npm", "start", "--", "--indexer", "--http" ] 25 | -------------------------------------------------------------------------------- /Dockerfile.web: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Bundle app source 6 | COPY src src 7 | COPY tsconfig.json ./ 8 | COPY vite.config.ts ./ 9 | COPY .eslintrc.cjs ./ 10 | COPY .prettierrc.json ./ 11 | COPY package*.json ./ 12 | 13 | RUN npm ci 14 | RUN npm run lint 15 | 16 | RUN npm run build 17 | 18 | RUN npm run test 19 | 20 | 21 | EXPOSE 8080 22 | 23 | 24 | CMD [ "npm", "start", "--", "--http", "--http-wait-for-sync=false" ] 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grants Stack Indexer 2 | 3 | The Grants Stack Indexer is a tool that indexes blockchain events generated by [Allo contracts](https://github.com/Allo-Protocol/contracts) and serves the data over HTTP in JSON format. The data is organized in a specific structure that enables easy access to different parts of the protocol. The indexer is built using [Chainsauce](https://github.com/boudra/chainsauce) and is designed to work with any EVM-compatible chain. 4 | 5 | The indexer data is used by [Grants Stack](https://github.com/gitcoinco/grants-stack) as a data layer. 6 | 7 | Indexer user docs featuring [sample queries](https://docs.allo.gitcoin.co/indexer/application) can be found at [docs.allo.gitcoin.co](https://docs.allo.gitcoin.co/indexer). 8 | 9 | ## API 10 | 11 | Access indexed data through the GraphQL endpoint at: 12 | 13 | https://grants-stack-indexer-v2.gitcoin.co/graphql 14 | 15 | Use the GraphiQL Playground to inspect the schema: 16 | 17 | https://grants-stack-indexer-v2.gitcoin.co/graphiql 18 | 19 | Indexed chains are defined in [config.ts](src/config.ts). 20 | 21 | ## Setup 22 | 23 | **Requires Node 18 minimum.** 24 | 25 | Copy `.env.example` to `.env`, review and optionally customize it. 26 | 27 | To pick chains to index, set `INDEXED_CHAINS` to a comma-separated list of chain identifiers. Available chain identifiers can be found in `src/config.ts`. 28 | 29 | ## Running in development 30 | 31 | ```bash 32 | cp .env.example .env 33 | docker-compose up -d 34 | npm install 35 | npm run build 36 | 37 | npm run dev 38 | 39 | # you can also pass arguments to the dev script 40 | 41 | npm run dev -- --drop-db # drop the database before indexing 42 | npm run dev -- --from-block=latest # start indexing from the last block 43 | npm run dev -- --from-block=12345 # start indexing from the 12345th block 44 | npm run dev -- --run-once # index and exit without watching for events 45 | npm run dev -- --no-cache # disable cache 46 | npm run dev -- --log-level=trace # set log level 47 | npm run dev -- --port=8081 # start web service on a given port 48 | ``` 49 | 50 | ## Running in production 51 | 52 | ```bash 53 | npm install 54 | npm run build 55 | npm run start # this will sync to the last block and then run the http server 56 | ``` 57 | 58 | Or use the provided `Dockerfile`. 59 | 60 | # Deployment 61 | 62 | Check out this guide to [deploy your own indexer on Fly.io](./docs/deploy-to-fly.md). 63 | 64 | We're currently continuously deploying the `main` branch into Fly. 65 | 66 | There are a few things to consider when it comes to deploying your changes: 67 | 68 | - Deployments resume indexing from the last indexed block, which means that your changes will only apply to new blocks, and migrations are not applied 69 | - If you need to change the database schema or change an event handler retroactively, you need to increment the `CHAIN_DATA_VERSION` constant found in [src/config.ts](https://github.com/gitcoinco/grants-stack-indexer/blob/main/src/config.ts), on deployment this will automatically create a new schema in Postgres and reindex all events. Note that deployments that reindex will take longer. 70 | -------------------------------------------------------------------------------- /READ_DOCKER_KB.md: -------------------------------------------------------------------------------- 1 | We followed this tutorial: https://www.digitalocean.com/community/tutorials/deploying-an-express-application-on-a-kubernetes-cluster 2 | 3 | - To build the docker images: 4 | 5 | ``` 6 | docker build -f Dockerfile.index -t indexer/index . 7 | docker build -f Dockerfile.web -t indexer/web . 8 | ``` 9 | 10 | - To push the images to docker hub: 11 | 12 | ``` 13 | docker tag indexer/index:latest gitcoinco/indexer-index:latest 14 | docker push gitcoinco/indexer-index:latest 15 | 16 | docker tag indexer/web:latest gitcoinco/indexer-web:latest 17 | docker push gitcoinco/indexer-web:latest 18 | ``` 19 | 20 | ### SOLUTION FOR ENVIRONMENT VARIABLES (kubernetes doesn't support .env file): 21 | 22 | - Created a config map: 23 | 24 | ``` 25 | kubectl create configmap indexer-web-config --from-env-file=.env 26 | ``` 27 | 28 | - Reference the config map in kb-deployment.yml with: 29 | 30 | ``` 31 | envFrom: 32 | - configMapRef: 33 | name: indexer-web-config 34 | ``` 35 | 36 | - NOT DONE - For sensitive data we could have creaded a secret with: 37 | 38 | ``` 39 | kubectl create secret generic indexer-web-secrets --from-env-file=.env 40 | ``` 41 | 42 | - NOT DONE - And referenced the secret in kb-deployment.yml with: 43 | 44 | ``` 45 | envFrom: 46 | - secretRef: 47 | name: indexer-web-secrets 48 | ``` 49 | 50 | ### Logs 51 | 52 | Get the current instances of server: 53 | 54 | ``` 55 | kubectl get pods 56 | ``` 57 | 58 | Possible response: 59 | 60 | ``` 61 | NAME READY STATUS RESTARTS AGE 62 | indexer-web-554574455d-lg98l 1/1 Running 0 41m 63 | indexer-web-554574455d-t4tgj 1/1 Running 0 41m 64 | ``` 65 | 66 | Then to see the logs of the first instance: 67 | 68 | ``` 69 | kubectl logs indexer-web-554574455d-lg98l 70 | ``` 71 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: postgres:13 6 | ports: 7 | - "5432:5432" 8 | environment: 9 | - POSTGRES_USER=postgres 10 | - POSTGRES_PASSWORD=postgres 11 | - POSTGRES_DB=grants_stack_indexer 12 | -------------------------------------------------------------------------------- /docs/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | ## Overview 4 | 5 | Caching allows us to optimize performance and reduce redundant operations. We have multiple caching mechanisms for different data. 6 | 7 | ### Types of Caches 8 | 9 | 1. **Chainsauce Cache** 10 | - We use the Chainsauce cache from [github.com/boudra/chainsauce](https://github.com/boudra/chainsauce), which caches contract events, contract reads, and block data. This cache is stored in SQLite on disk, reducing the need for redundant blockchain interactions and speeding up data retrieval processes. 11 | 12 | 2. **IPFS Cache** 13 | - Data fetched from IPFS is cached on disk using `make-fetch-happen`. This ensures that we avoid repeated network calls, providing faster access to IPFS resources and reducing bandwidth usage. 14 | 15 | 3. **CoinGecko HTTP Calls Cache** 16 | - HTTP calls to the CoinGecko API are also cached on disk via `make-fetch-happen`. This minimizes repeated API calls and improves the performance of our application by reducing the latency of fetching exchange rates and other cryptocurrency data. 17 | 18 | ### Cache Storage Location 19 | 20 | - **Production**: In production, all caches are stored in `/mnt/indexer/cache`. 21 | - **Development**: In development, caches are stored inside `.var/cache` in the indexer working directory. 22 | 23 | ## Potential Improvements 24 | 25 | 1. **Integrating All Caches Inside the SQLite Database**: 26 | - By integrating all caches into the SQLite database, we can make the cache portable. Users deploying their own indexers can pull the cache from S3 and start with a warm cache, significantly speeding up indexing time. 27 | 28 | 2. **Moving the Cache to PostgreSQL**: 29 | - Moving the cache to PostgreSQL would allow it to be shared by all instances of the application. Portability could be achieved using `pgdump`, enabling users to transfer the cache easily and maintain consistency across different deployments. 30 | -------------------------------------------------------------------------------- /docs/deploy-to-fly.md: -------------------------------------------------------------------------------- 1 | # Deploy to Fly 2 | 3 | We use Fly.io to deploy the indexer. This guide will walk you through the process of setting up and deploying your own indexer on Fly.io. 4 | 5 | ## Getting Started 6 | 7 | First, ensure you have `flyctl` installed by following the [official installation guide](https://fly.io/docs/hands-on/install-flyctl/). 8 | 9 | ## Configuration 10 | 11 | Next, clone the repository and make necessary adjustments to the `fly.toml` file: 12 | 13 | 1. Adjust `INDEXED_CHAINS` in the configuration to include the chains you want to index. Supported chains can be found in `src/config.ts`. 14 | 2. The indexer defaults to using public RPCs. Due to their low rate limits, it's advisable to use private RPCs. Define your RPC URLs in environment variables formatted as `${CHAIN_NAME}_RPC_URL`, such as `OPTIMISM_RPC_URL` for Optimism. 15 | 16 | ## Deployment 17 | 18 | After configuring, deploy your app with the launch command. It will detect the existing `fly.toml` in the repository and make a new app based on that. It will also launch a Postgres database and automatically add the `DATABASE_URL` secret for you. 19 | 20 | You might want to specify the `--org` parameter to launch the app in the right organization. Run `fly orgs list` to see the orgs you're part of. 21 | 22 | ```sh 23 | fly launch --copy-config 24 | ``` 25 | 26 | Monitor the deployment process and troubleshoot if necessary by running: 27 | 28 | ```sh 29 | fly logs 30 | ``` 31 | 32 | Note that because of the number of events Allo contracts emit, the first run might take hours. Depending on the RPCs, it might even fail. Ensure you monitor progress and update RPCs if necessary. 33 | 34 | ## Access Your Indexer 35 | 36 | Once it's live, Fly will provide a URL where you can access your indexer. 37 | -------------------------------------------------------------------------------- /docs/devops.md: -------------------------------------------------------------------------------- 1 | The application is deployed as **indexer-v2** on Fly under the [Gitcoin organization](https://fly.io/dashboard/gtc). If you do not have access to this dashboard, please request permission. 2 | 3 | # Fly CLI Installation 4 | 5 | Before deploying or managing the application, ensure you have the Fly CLI installed. Follow these steps to install the Fly CLI: 6 | 7 | 1. Download the Fly CLI from [Fly's official documentation](https://fly.io/docs/getting-started/installing-flyctl/). 8 | 2. Install the CLI following the instructions for your operating system. 9 | 3. Once installed, run `flyctl auth login` to authenticate with your Fly account. 10 | 11 | # Components and Architecture 12 | 13 | The [application](../fly.toml) contains two main components organized into separate process groups: 14 | 15 | - **web**: Handles client requests and must scale horizontally to manage varying loads effectively. 16 | - **indexer**: Responsible for updating and maintaining the database. Only one active instance runs due to its role as the sole writer to prevent data conflicts. 17 | 18 | Run `fly status` too see an overview of the deployment. 19 | 20 | # Deployment Process 21 | 22 | Check the [Github Workflow](../.github/workflows/deploy-branch.yml) to understand how the deployment works. 23 | 24 | ### General Recommendation 25 | 26 | - Always deploy through the GitHub workflow to ensure all changes are tracked and reversible. 27 | - Avoid deploying directly from the CLI as it will deploy your working directory and changes might be deployed accidentally. 28 | 29 | ### Deployment Checks 30 | 31 | - Use `fly status` to check the status of the instances. 32 | - Although two Indexer instances are running, only one actively indexes due to a locking mechanism. 33 | 34 | ## Lock Mechanism and High Availability 35 | 36 | - A write lock ensures that while multiple indexer instances may be present for failover readiness, only one actively indexes at any time. 37 | - This is especially crucial during updates: 38 | - If updating from v50 to v51, two v50 instances may run; one indexes while the other remains idle. 39 | - Deploying v51 will stop an instance of v50 and start v51, maintaining continuous indexing and high availability. 40 | 41 | # Operational Tasks 42 | 43 | ## Logs 44 | 45 | Logs are shipped to [Datadog](https://app.datadoghq.eu/logs). 46 | 47 | Here are some examples of queries you can run on Datadog. 48 | 49 | All logs for app `indexer-v2`: 50 | 51 | ``` 52 | @fly.app.name:indexer-v2 53 | ``` 54 | 55 | All logs for app `indexer-v2` and instance `17816e3ea42e48`: 56 | 57 | ``` 58 | @fly.app.name:indexer-v2 @fly.app.instance:17816e3ea42e48 59 | ``` 60 | 61 | All errors: 62 | 63 | ``` 64 | @fly.app.name:indexer-v2 status:error 65 | ``` 66 | 67 | All logs for chain 1: 68 | 69 | ``` 70 | @fly.app.name:indexer-v2 @chain:1 71 | ``` 72 | 73 | You can also check the latest logs with the [Fly logs](https://fly.io/docs/flyctl/logs/) command. 74 | 75 | ## Scaling 76 | 77 | ### Fly Web Auto-Scaling 78 | 79 | There are a couple of web instances provisioned, the stopped ones are on stand-by. Fly automatically starts and stops machines based on load. 80 | 81 | ### Horizontal Scaling 82 | 83 | Use the `scale` command to scale up and down the number of machines: 84 | 85 | 86 | ``` 87 | flyctl scale web=5 88 | ``` 89 | 90 | ### Vertical Scaling 91 | 92 | Show the current CPU and RAM configuration of the machines: 93 | 94 | ``` 95 | flyctl scale show 96 | ``` 97 | 98 | Check the available VM size presets: 99 | 100 | ``` 101 | flyctl platform vm-sizes 102 | ``` 103 | 104 | Change the VM size: 105 | 106 | 107 | ``` 108 | flyctl scale vm performance-1x 109 | ``` 110 | 111 | Change memory: 112 | 113 | ``` 114 | flyctl scale memory 2048 115 | ``` 116 | 117 | Increase volume size: 118 | ```bash 119 | # to get the ids of the indexer machines 120 | flyctl -c fly.toml status 121 | 122 | # Check the volumes 123 | flyctl -c fly.toml volumes list 124 | 125 | # You will see each one is attached to a machine, 126 | # so you will know the volumes attached to indexer machines. 127 | # Use the volume ID as needed from above. 128 | 129 | # To increase the volume size 130 | flyctl -c fly.toml volumes extend ADD_HERE_A_VOLUME_ID -s 5 # to extend it to 5GB 131 | ``` 132 | 133 | # Monitoring Performance 134 | 135 | Fly hosts a Grafana dashboard [here](https://fly-metrics.net/d/fly-app/fly-app?orgId=179263&var-app=indexer-v2) with LB and instance level metrics. 136 | -------------------------------------------------------------------------------- /docs/donations.md: -------------------------------------------------------------------------------- 1 | # Donation Indexing 2 | 3 | Handling donations and stats efficiently is crucial, especially when reindexing. With over 1 million votes, live indexing isn't a major issue, but reindexing poses challenges. 4 | 5 | It's critical to ensure that any changes to the donation event handler do not slow down the indexing process, as this would significantly impact overall indexing time. 6 | 7 | ### Challenges 8 | 9 | 1. **Keeping Stats Up to Date** Each donation updates stats at the application and round level. Calculating these stats on each donation is not feasible. 10 | 2. **Donation Conversion**: Each donation amount needs to be converted to USD and round tokens. 11 | 3. **Timestamp Handling**: `getLogs` does not give you timestamps for each event. We must fetch the timestamp for each donation using an RPC call to `getBlock`. 12 | 4. **Database Insertion**: Each vote must be inserted into the database. 13 | 14 | ### Performance Considerations 15 | 16 | ### Caching 17 | 18 | - **Logs Caching**: Donation logs are cached to avoid having to call the RPCs. 19 | - **USD Prices Caching**: Exchange rates for converting donations to USD are cached. 20 | - **getBlock Calls Caching**: The results of `getBlock` calls are cached to minimize RPC calls. 21 | 22 | ### Donation Batching 23 | 24 | - **Batch Insertions**: Donations are not inserted individually. Instead, they are queued and batch inserted 1000 at a time. This reduces the number of database round trips, improving performance. 25 | 26 | ### Stats Updater 27 | 28 | - **Deferred Stats Calculation**: Stats are updated in batches rather than on each donation. This helps in maintaining application and round-level stats efficiently without frequent recalculations. 29 | - **Throttled Updates**: Stats updating is throttled to occur outside of the event handler. Since stats do not need to be immediately up-to-date, they can be updated at intervals (e.g., every minute). Refer to the `Database` class for more details. 30 | 31 | ```mermaid 32 | graph TD 33 | A[Events from Contract] --> B[Call Event Handler] 34 | B --> C[Get Price from Cache] 35 | B --> D[Get Block Timestamp from Cache] 36 | B --> E[Push Donation to Queue] 37 | E --> F[Batch Insert Every Few Seconds] 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/reindexing.md: -------------------------------------------------------------------------------- 1 | # Reindexing 2 | 3 | ## Deployment Considerations 4 | 5 | When deploying changes to the indexer, it's important to clarify the results you want to achieve: 6 | 7 | 1. **Applying Changes to New Blocks Only**: 8 | - If you want your changes to apply only to new blocks, or your changes don't affect the data itself (e.g. API changes), simply deploy your changes. The indexer will resume indexing from the last indexed block without affecting previously indexed data. Migrations will not be applied. 9 | 10 | 2. **Changing Database Schema or Event Handlers Retroactively**: 11 | - If you need to change the database schema or modify event handlers for previously indexed blocks, you must increment the `CHAIN_DATA_VERSION` constant in `src/config.ts`. 12 | - The indexer will create a new schema in Postgres named `chain_data_${version}`. If this schema does not exist, it will be created, all necessary tables will be set up, and indexing will start from scratch. 13 | - If the schema already exists, the indexer will resume indexing from the last indexed block unless the `--drop-db` flag is specified via the CLI. This will drop the existing database and start fresh. 14 | 15 | ### Dropping Schemas in Development 16 | 17 | - During development, you can use the `--drop-db` flag to ensure the indexer always deletes all existing schema and migrates from scratch. This can be useful for testing schema changes and event handler modifications without retaining old data. 18 | 19 | - During development, you can use the `--drop-chain-db` flag to ensure the indexer always deletes chain schema and migrates from scratch. 20 | 21 | - During development, you can use the `--drop-ipfs-db` flag to ensure the indexer always deletes ipfs schema and migrates from scratch. 22 | 23 | - During development, you can use the `--drop-price-db` flag to ensure the indexer always deletes price schema and migrates from scratch. 24 | 25 | ### Important Notes 26 | 27 | - **Reindexing Time**: Deployments involving reindexing will take significantly longer. Plan accordingly to minimize downtime or performance impact. 28 | - **Reindexing Monitoring:** Make sure that you monitor reindexing progress through DataDog. 29 | 30 | 31 | Use the status endpoint to see what schema version we're serving: https://grants-stack-indexer-v2.gitcoin.co/api/v1/status 32 | -------------------------------------------------------------------------------- /docs/write-lock.md: -------------------------------------------------------------------------------- 1 | # Indexer Write Lock Mechanism 2 | 3 | ### Exclusive Indexing 4 | 5 | - **Write Lock**: The indexing process is exclusive, meaning only one indexer can write at any given time. This prevents data corruption by ensuring that simultaneous writes do not occur. 6 | - **High Availability**: The locking mechanism also facilitates high availability. If the active indexer crashes, another indexer can acquire the lock and continue indexing. 7 | 8 | ### Deployment Strategy 9 | 10 | 1. **Two Indexers Running**: 11 | - We have two indexers running concurrently. One indexer actively holds the write lock and performs indexing, while the other remains on standby, ready to take over if the active indexer fails. 12 | 13 | 2. **Fly Rolling Deployment**: 14 | - When deploying updates, we use the fly rolling strategy. This process updates one instance at a time: 15 | - First, it updates one indexer. If the update is successful, it then updates the second indexer. 16 | - This ensures continuous service availability during deployments. 17 | 18 | 3. **Reindexing Process**: 19 | - When reindexing (writing to a new schema), the old indexer continues to update the old schema, while the new indexer writes to the new schema. 20 | - Since they write to different schemas, this allows the web service to keep serving data from the old schema while the new indexer performs reindexing. 21 | - This separation ensures a zero-downtime upgrade, as the reindexing process can take a significant amount of time. 22 | -------------------------------------------------------------------------------- /ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "indexer", 5 | script: "npm", 6 | args: "start -- --indexer --http", 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /fly.log-shipper.toml: -------------------------------------------------------------------------------- 1 | app = "indexer-log-shipper" 2 | primary_region = "den" 3 | 4 | [build] 5 | image = "ghcr.io/superfly/fly-log-shipper:latest" 6 | 7 | [services] 8 | http_checks = [] 9 | internal_port = 8686 10 | 11 | [env] 12 | ORG = "gtc" 13 | DATADOG_SITE = "datadoghq.eu" 14 | -------------------------------------------------------------------------------- /fly.staging.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for indexer-v2-staging on 2024-05-21T18:42:41+05:30 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'indexer-v2-staging' 7 | primary_region = 'den' 8 | kill_signal = 'SIGINT' 9 | kill_timeout = '5s' 10 | 11 | [experimental] 12 | auto_rollback = true 13 | 14 | [build] 15 | 16 | [deploy] 17 | wait_timeout = '6h0m0s' 18 | 19 | [env] 20 | DEPLOYMENT_ENVIRONMENT = 'production' 21 | ENABLE_RESOURCE_MONITOR = 'false' 22 | ESTIMATES_LINEARQF_WORKER_POOL_SIZE = '10' 23 | INDEXED_CHAINS = 'sepolia' 24 | LOG_LEVEL = 'debug' 25 | NODE_OPTIONS = '--max-old-space-size=4096' 26 | PASSPORT_SCORER_ID = '335' 27 | PINO_PRETTY = 'true' 28 | PORT = '8080' 29 | STORAGE_DIR = '/mnt/indexer' 30 | 31 | [processes] 32 | indexer = 'npm start -- --indexer --http' 33 | web = 'npm start -- --http --http-wait-for-sync=false' 34 | 35 | [[mounts]] 36 | source = 'indexer_staging' 37 | destination = '/mnt/indexer' 38 | initial_size = '50gb' 39 | processes = ['indexer', 'web'] 40 | 41 | [http_service] 42 | internal_port = 8080 43 | force_https = true 44 | auto_stop_machines = true 45 | auto_start_machines = true 46 | min_machines_running = 2 47 | processes = ['web'] 48 | 49 | [http_service.concurrency] 50 | type = 'requests' 51 | hard_limit = 250 52 | soft_limit = 200 53 | 54 | [checks] 55 | [checks.http] 56 | port = 8080 57 | type = 'http' 58 | interval = '15s' 59 | timeout = '10s' 60 | grace_period = '30s' 61 | method = 'get' 62 | path = '/api/v1/status' 63 | processes = ['web', 'indexer'] 64 | 65 | [[vm]] 66 | size = 'performance-2x' 67 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | 2 | app = "indexer-v2" 3 | primary_region = "den" 4 | kill_signal = "SIGINT" 5 | kill_timeout = "5s" 6 | 7 | [experimental] 8 | auto_rollback = true 9 | 10 | [build] 11 | 12 | [deploy] 13 | wait_timeout = "6h0m0s" 14 | 15 | [env] 16 | PINO_PRETTY = "true" 17 | DEPLOYMENT_ENVIRONMENT = "production" 18 | ENABLE_RESOURCE_MONITOR = "false" 19 | ESTIMATES_LINEARQF_WORKER_POOL_SIZE = "10" 20 | INDEXED_CHAINS = "mainnet,optimism,fantom,arbitrum,polygon,sepolia,avalanche,avalanche-fuji,scroll,scroll-sepolia,base,zksync-era-mainnet,lukso-mainnet,lukso-testnet,celo-mainnet,celo-testnet,metisAndromeda,gnosis,sei-mainnet,hedera" 21 | LOG_LEVEL = "debug" 22 | NODE_OPTIONS = "--max-old-space-size=4096" 23 | PORT = "8080" 24 | STORAGE_DIR = "/mnt/indexer" 25 | PASSPORT_SCORER_ID = 335 26 | 27 | [processes] 28 | indexer = "npm start -- --indexer --http" 29 | web = "npm start -- --http --http-wait-for-sync=false" 30 | 31 | [[mounts]] 32 | source = "indexer_staging" 33 | destination = "/mnt/indexer" 34 | initial_size = "50GB" 35 | auto_extend_size_threshold = 80 36 | auto_extend_size_increment = "5GB" 37 | auto_extend_size_limit = "100GB" 38 | processes = ["indexer", "web"] 39 | 40 | [[services]] 41 | internal_port = 8080 42 | processes = ["indexer"] 43 | protocol = "tcp" 44 | script_checks = [] 45 | 46 | [services.concurrency] 47 | hard_limit = 250 48 | soft_limit = 200 49 | type = "requests" 50 | 51 | [[services.ports]] 52 | force_https = true 53 | handlers = ["http"] 54 | port = 8080 55 | 56 | [[services.ports]] 57 | handlers = ["tls", "http"] 58 | port = 8081 59 | 60 | [[services.tcp_checks]] 61 | grace_period = "30s" 62 | interval = "15s" 63 | restart_limit = 0 64 | timeout = "10s" 65 | 66 | [[services]] 67 | internal_port = 8080 68 | processes = ["web"] 69 | protocol = "tcp" 70 | script_checks = [] 71 | 72 | [services.concurrency] 73 | hard_limit = 250 74 | soft_limit = 200 75 | type = "requests" 76 | 77 | [[services.ports]] 78 | force_https = true 79 | handlers = ["http"] 80 | port = 80 81 | 82 | [[services.ports]] 83 | handlers = ["tls", "http"] 84 | port = 443 85 | 86 | [[services.tcp_checks]] 87 | grace_period = "30s" 88 | interval = "15s" 89 | restart_limit = 0 90 | timeout = "10s" 91 | 92 | [checks.http] 93 | port = 8080 94 | type = "http" 95 | interval = "15s" 96 | timeout = "10s" 97 | grace_period = "30s" 98 | method = "get" 99 | path = "/api/v1/status" 100 | processes = ["web", "indexer"] 101 | 102 | [[vm]] 103 | memory = "4gb" 104 | cpu_kind = "performance" 105 | cpus = 2 -------------------------------------------------------------------------------- /indexer-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | web: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.web 8 | ports: 9 | - "8081:8080" # Map the port your web server uses 10 | depends_on: 11 | - db 12 | environment: 13 | INDEXED_CHAINS: ${INDEXED_CHAINS} 14 | PASSPORT_SCORER_ID: ${PASSPORT_SCORER_ID} 15 | STORAGE_DIR: ${STORAGE_DIR} 16 | DEPLOYMENT_ENVIRONMENT: ${DEPLOYMENT_ENVIRONMENT} 17 | PORT: ${PORT} 18 | LOG_LEVEL: ${LOG_LEVEL} 19 | BUILD_TAG: ${BUILD_TAG} 20 | ENABLE_RESOURCE_MONITOR: ${ENABLE_RESOURCE_MONITOR} 21 | ESTIMATES_LINEARQF_WORKER_POOL_SIZE: ${ESTIMATES_LINEARQF_WORKER_POOL_SIZE} 22 | PINO_PRETTY: ${PINO_PRETTY} 23 | IPFS_GATEWAYS: ${IPFS_GATEWAYS} 24 | COINGECKO_API_KEY: ${COINGECKO_API_KEY} 25 | GRAPHILE_LICENSE: ${GRAPHILE_LICENSE} 26 | SEPOLIA_RPC_URL: ${SEPOLIA_RPC_URL} 27 | POLYGON_MUMBAI_RPC_URL: ${POLYGON_MUMBAI_RPC_URL} 28 | AVALANCHE_RPC_URL: ${AVALANCHE_RPC_URL} 29 | OPTIMISM_RPC_URL: ${OPTIMISM_RPC_URL} 30 | SENTRY_DSN: ${SENTRY_DSN} 31 | PGN_TESTNET_RPC_URL: ${PGN_TESTNET_RPC_URL} 32 | ARBITRUM_GOERLI_RPC_URL: ${ARBITRUM_GOERLI_RPC_URL} 33 | FANTOM_RPC_URL: ${FANTOM_RPC_URL} 34 | BASE_RPC_URL: ${BASE_RPC_URL} 35 | PGN_RPC_URL: ${PGN_RPC_URL} 36 | GOERLI_RPC_URL: ${GOERLI_RPC_URL} 37 | AVALANCHE_FUJI_RPC_URL: ${AVALANCHE_FUJI_RPC_URL} 38 | ARBITRUM_RPC_URL: ${ARBITRUM_RPC_URL} 39 | SEI_MAINNET_RPC_URL: ${SEI_MAINNET_RPC_URL} 40 | MAINNET_RPC_URL: ${MAINNET_RPC_URL} 41 | POLYGON_RPC_URL: ${POLYGON_RPC_URL} 42 | METIS_ANDROMEDA_RPC_URL: ${METIS_ANDROMEDA_RPC_URL} 43 | SCROLL_SEPOLIA_RPC_URL: ${SCROLL_SEPOLIA_RPC_URL} 44 | DATABASE_URL: "postgresql://postgres:postgres@db:5432/grants_stack_indexer" 45 | 46 | index: 47 | build: 48 | context: . 49 | dockerfile: Dockerfile.index 50 | ports: 51 | - "8080:8080" # Map the port your web server uses 52 | depends_on: 53 | - db 54 | environment: 55 | INDEXED_CHAINS: ${INDEXED_CHAINS} 56 | PASSPORT_SCORER_ID: ${PASSPORT_SCORER_ID} 57 | STORAGE_DIR: ${STORAGE_DIR} 58 | DEPLOYMENT_ENVIRONMENT: ${DEPLOYMENT_ENVIRONMENT} 59 | PORT: ${PORT} 60 | LOG_LEVEL: ${LOG_LEVEL} 61 | BUILD_TAG: ${BUILD_TAG} 62 | ENABLE_RESOURCE_MONITOR: ${ENABLE_RESOURCE_MONITOR} 63 | ESTIMATES_LINEARQF_WORKER_POOL_SIZE: ${ESTIMATES_LINEARQF_WORKER_POOL_SIZE} 64 | PINO_PRETTY: ${PINO_PRETTY} 65 | IPFS_GATEWAYS: ${IPFS_GATEWAYS} 66 | COINGECKO_API_KEY: ${COINGECKO_API_KEY} 67 | GRAPHILE_LICENSE: ${GRAPHILE_LICENSE} 68 | SEPOLIA_RPC_URL: ${SEPOLIA_RPC_URL} 69 | POLYGON_MUMBAI_RPC_URL: ${POLYGON_MUMBAI_RPC_URL} 70 | AVALANCHE_RPC_URL: ${AVALANCHE_RPC_URL} 71 | OPTIMISM_RPC_URL: ${OPTIMISM_RPC_URL} 72 | SENTRY_DSN: ${SENTRY_DSN} 73 | PGN_TESTNET_RPC_URL: ${PGN_TESTNET_RPC_URL} 74 | ARBITRUM_GOERLI_RPC_URL: ${ARBITRUM_GOERLI_RPC_URL} 75 | FANTOM_RPC_URL: ${FANTOM_RPC_URL} 76 | BASE_RPC_URL: ${BASE_RPC_URL} 77 | PGN_RPC_URL: ${PGN_RPC_URL} 78 | GOERLI_RPC_URL: ${GOERLI_RPC_URL} 79 | AVALANCHE_FUJI_RPC_URL: ${AVALANCHE_FUJI_RPC_URL} 80 | ARBITRUM_RPC_URL: ${ARBITRUM_RPC_URL} 81 | SEI_MAINNET_RPC_URL: ${SEI_MAINNET_RPC_URL} 82 | MAINNET_RPC_URL: ${MAINNET_RPC_URL} 83 | POLYGON_RPC_URL: ${POLYGON_RPC_URL} 84 | METIS_ANDROMEDA_RPC_URL: ${METIS_ANDROMEDA_RPC_URL} 85 | SCROLL_SEPOLIA_RPC_URL: ${SCROLL_SEPOLIA_RPC_URL} 86 | DATABASE_URL: "postgresql://postgres:postgres@db:5432/grants_stack_indexer" 87 | 88 | db: 89 | image: postgres:15 # Use the version of PostgreSQL you need 90 | environment: 91 | POSTGRES_USER: postgres 92 | POSTGRES_PASSWORD: postgres 93 | POSTGRES_DB: grants_stack_indexer 94 | volumes: 95 | - db_data:/var/lib/postgresql/data 96 | 97 | volumes: 98 | db_data: 99 | -------------------------------------------------------------------------------- /kb-deployment.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: indexer-web 5 | spec: 6 | replicas: 2 7 | selector: 8 | matchLabels: 9 | app: indexer-web 10 | template: 11 | metadata: 12 | labels: 13 | app: indexer-web 14 | spec: 15 | containers: 16 | - name: indexer-web 17 | image: "gitcoinco/indexer-web:latest" 18 | ports: 19 | - containerPort: 80 20 | envFrom: 21 | - configMapRef: 22 | name: indexer-web-config 23 | -------------------------------------------------------------------------------- /kb-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: load-balancer 5 | labels: 6 | app: indexer-web 7 | spec: 8 | type: LoadBalancer 9 | ports: 10 | - name: http 11 | port: 80 12 | targetPort: 8080 13 | - name: https 14 | port: 443 15 | targetPort: 8080 16 | selector: 17 | app: indexer-web 18 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | prettier: 5 | glob: "*.{js,ts,jsx,tsx}" 6 | run: npx prettier --write {staged_files} 7 | 8 | pre-push: 9 | parallel: true 10 | commands: 11 | eslint: 12 | glob: "*.{js,ts,jsx,tsx}" 13 | run: npm run lint {staged_files} 14 | typecheck: 15 | glob: "*.{js,ts,jsx,tsx}" 16 | run: npm run typecheck 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitcoin-indexer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "dist/src/indexer/index.js", 7 | "types": "dist/src/indexer/index.d.ts", 8 | "scripts": { 9 | "start": "node dist/src/index.js", 10 | "dev": "tsx watch src/index.ts --http --indexer --http-wait-for-sync=false", 11 | "build": "tsc", 12 | "lint": "eslint src && prettier --check src", 13 | "format": "prettier --write src", 14 | "test": "vitest run --reporter verbose", 15 | "test:watch": "vitest watch --reporter verbose", 16 | "typecheck": "tsc --noEmit", 17 | "typecheck:watch": "tsc --noEmit --watch", 18 | "deploy:development": "docker buildx build . --platform=linux/amd64 -t registry.fly.io/indexer-development:latest && docker push registry.fly.io/indexer-development:latest && flyctl -c fly.development.toml --app indexer-development deploy -i registry.fly.io/indexer-development:latest" 19 | }, 20 | "imports": { 21 | "#abis/*": { 22 | "default": "./src/indexer/abis/*" 23 | } 24 | }, 25 | "author": "", 26 | "license": "ISC", 27 | "dependencies": { 28 | "@graphile-contrib/pg-simplify-inflector": "^6.1.0", 29 | "@graphile/pro": "^1.0.4", 30 | "@isaacs/ttlcache": "^1.4.1", 31 | "@sentry/node": "^7.51.0", 32 | "@teamawesome/tiny-batch": "^1.0.7", 33 | "async-retry": "^1.3.3", 34 | "better-sqlite3": "^8.6.0", 35 | "chainsauce": "github:gitcoinco/chainsauce#main", 36 | "cors": "^2.8.5", 37 | "csv-parser": "^3.0.0", 38 | "csv-writer": "^1.6.0", 39 | "diskstats": "^0.1.0", 40 | "dotenv": "^16.0.3", 41 | "ethers": "^5.7.2", 42 | "express": "^4.18.2", 43 | "express-async-errors": "^3.1.1", 44 | "fastq": "^1.15.0", 45 | "fetch-retry": "^5.0.6", 46 | "hot-shots": "^10.0.0", 47 | "into-stream": "^8.0.0", 48 | "kysely": "^0.26.3", 49 | "level": "^8.0.0", 50 | "lru-cache": "^10.1.0", 51 | "make-fetch-happen": "^11.1.1", 52 | "multer": "^1.4.5-lts.1", 53 | "node-worker-threads-pool": "^1.5.1", 54 | "pg": "^8.11.3", 55 | "pino": "^8.14.1", 56 | "pino-http": "^8.5.1", 57 | "pino-pretty": "^10.0.1", 58 | "pluralistic": "github:gitcoinco/pluralistic.js#322323c7440db955966f3cf523bbb84ca60894f9", 59 | "postgraphile": "^4.13.0", 60 | "postgraphile-plugin-connection-filter": "^2.3.0", 61 | "postgres": "^3.4.3", 62 | "serve-index": "^1.9.1", 63 | "sleep-promise": "^9.1.0", 64 | "split2": "^4.2.0", 65 | "statuses-bitmap": "github:gitcoinco/statuses-bitmap#f123d7778e42e16adb98fff2b2ba18c0fee57227", 66 | "stream-to-string": "^1.2.1", 67 | "throttle-debounce": "^5.0.0", 68 | "tmp": "^0.2.1", 69 | "ts-essentials": "^9.4.1", 70 | "ts-is-present": "^1.2.2", 71 | "viem": "^1.16.4", 72 | "write-file-atomic": "^5.0.1", 73 | "zod": "^3.21.4" 74 | }, 75 | "devDependencies": { 76 | "@types/async-retry": "^1.4.8", 77 | "@types/better-sqlite3": "^7.6.5", 78 | "@types/cors": "^2.8.13", 79 | "@types/express": "^4.17.17", 80 | "@types/make-fetch-happen": "^10.0.1", 81 | "@types/memory-cache": "^0.2.2", 82 | "@types/multer": "^1.4.7", 83 | "@types/node": "^18.15.3", 84 | "@types/pg": "^8.10.9", 85 | "@types/serve-index": "^1.9.1", 86 | "@types/split2": "^4.2.0", 87 | "@types/supertest": "^2.0.12", 88 | "@types/throttle-debounce": "^5.0.0", 89 | "@types/tmp": "^0.2.3", 90 | "@types/write-file-atomic": "^4.0.0", 91 | "@typescript-eslint/eslint-plugin": "^5.55.0", 92 | "@typescript-eslint/parser": "^5.55.0", 93 | "concurrently": "^8.2.0", 94 | "eslint": "^8.36.0", 95 | "eslint-plugin-no-only-tests": "^3.1.0", 96 | "lefthook": "^1.4.8", 97 | "prettier": "^3.0.1", 98 | "supertest": "^6.3.3", 99 | "tsx": "^3.12.7", 100 | "typescript": "^5.2.2", 101 | "vitest": "0.34.5" 102 | }, 103 | "engines": { 104 | "node": "20.x" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/address.ts: -------------------------------------------------------------------------------- 1 | export type Address = `0x${string}` & { __brand: "Address" }; 2 | 3 | class InvalidAddress extends Error { 4 | constructor(address: string) { 5 | super(`Invalid address: ${address}`); 6 | } 7 | } 8 | 9 | export function safeParseAddress(address: string): Address | null { 10 | if (address.startsWith("0x") === false) { 11 | return null; 12 | } 13 | 14 | if (address.length !== 42) { 15 | return null; 16 | } 17 | 18 | return address.toLowerCase() as Address; 19 | } 20 | 21 | export function parseAddress(address: string): Address { 22 | const parsed = safeParseAddress(address); 23 | if (!parsed) { 24 | throw new InvalidAddress(address); 25 | } 26 | return parsed; 27 | } 28 | -------------------------------------------------------------------------------- /src/blockCache.ts: -------------------------------------------------------------------------------- 1 | export type Block = { 2 | chainId: number; 3 | blockNumber: bigint; 4 | timestampInSecs: number; 5 | }; 6 | 7 | export interface BlockCache { 8 | getTimestampByBlockNumber( 9 | chainId: number, 10 | blockNumber: bigint 11 | ): Promise; 12 | getBlockNumberByTimestamp( 13 | chainId: number, 14 | timestampInSecs: number 15 | ): Promise; 16 | saveBlock(block: Block): Promise; 17 | getClosestBoundsForTimestamp( 18 | chainId: number, 19 | timestampInSecs: number 20 | ): Promise<{ before: Block | null; after: Block | null }>; 21 | } 22 | -------------------------------------------------------------------------------- /src/calculator/calculateMatches.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "pino"; 2 | import { PassportProvider } from "../passport/index.js"; 3 | import { DataProvider } from "./dataProvider/index.js"; 4 | import { ResourceNotFoundError } from "./errors.js"; 5 | import { PriceProvider } from "../prices/provider.js"; 6 | import { aggregateContributions } from "./votes.js"; 7 | import { Calculation, linearQFWithAggregates } from "pluralistic"; 8 | import { ProportionalMatchOptions } from "./options.js"; 9 | import { CoefficientOverrides } from "./coefficientOverrides.js"; 10 | import { Chain } from "../config.js"; 11 | import { 12 | CalculationConfig, 13 | extractCalculationConfigFromRound, 14 | overrideCalculationConfig, 15 | } from "./calculationConfig.js"; 16 | import { convertTokenToFiat } from "../tokenMath.js"; 17 | import { parseAddress } from "../address.js"; 18 | import { 19 | DeprecatedApplication, 20 | DeprecatedRound, 21 | DeprecatedVote, 22 | } from "../deprecatedJsonDatabase.js"; 23 | 24 | export type CalculateMatchesConfig = { 25 | roundId: string; 26 | calculationConfigOverride?: Partial; 27 | coefficientOverrides: CoefficientOverrides; 28 | chain: Chain; 29 | proportionalMatch?: ProportionalMatchOptions; 30 | deps: { 31 | passportProvider: PassportProvider; 32 | dataProvider: DataProvider; 33 | priceProvider: PriceProvider; 34 | logger: Logger; 35 | }; 36 | }; 37 | 38 | export type AugmentedResult = Calculation & { 39 | projectId: string; 40 | applicationId: string; 41 | matchedUSD: number; 42 | projectName?: string; 43 | payoutAddress?: string; 44 | }; 45 | 46 | export const calculateMatches = async ( 47 | params: CalculateMatchesConfig 48 | ): Promise => { 49 | const { 50 | calculationConfigOverride, 51 | coefficientOverrides, 52 | chain, 53 | roundId, 54 | proportionalMatch, 55 | deps: { passportProvider, dataProvider, priceProvider }, 56 | } = params; 57 | 58 | const applications = await dataProvider.loadFile( 59 | "applications", 60 | `${chain.id}/rounds/${roundId}/applications.json` 61 | ); 62 | 63 | const votes = await dataProvider.loadFile( 64 | "votes", 65 | `${chain.id}/rounds/${roundId}/votes.json` 66 | ); 67 | 68 | const rounds = await dataProvider.loadFile( 69 | "rounds", 70 | `${chain.id}/rounds.json` 71 | ); 72 | 73 | const round = rounds.find((round) => round.id === params.roundId); 74 | 75 | if (round === undefined) { 76 | throw new ResourceNotFoundError("round"); 77 | } 78 | 79 | const roundCalculationConfig = extractCalculationConfigFromRound(round); 80 | 81 | const calculationConfig: CalculationConfig = overrideCalculationConfig( 82 | roundCalculationConfig, 83 | calculationConfigOverride ?? {} 84 | ); 85 | 86 | const passportScoreByAddress = await passportProvider.getScoresByAddresses( 87 | votes.map((vote) => vote.voter.toLowerCase()) 88 | ); 89 | 90 | const roundTokenPriceInUsd = await priceProvider.getUSDConversionRate( 91 | chain.id, 92 | parseAddress(round.token), 93 | "latest" 94 | ); 95 | 96 | const aggregatedContributions = aggregateContributions({ 97 | chain, 98 | round, 99 | votes, 100 | applications, 101 | passportScoreByAddress, 102 | enablePassport: calculationConfig.enablePassport, 103 | minimumAmountUSD: calculationConfig.minimumAmountUSD, 104 | coefficientOverrides, 105 | proportionalMatchOptions: proportionalMatch, 106 | }); 107 | 108 | const results = linearQFWithAggregates( 109 | aggregatedContributions, 110 | calculationConfig.matchAmount, 111 | 0n, // not used, should be deleted in Pluralistic 112 | { 113 | minimumAmount: 0n, // we're filtering by minimum amount in aggregateContributions 114 | matchingCapAmount: calculationConfig.matchingCapAmount, 115 | ignoreSaturation: calculationConfig.ignoreSaturation ?? false, 116 | } 117 | ); 118 | 119 | const augmented: Array = []; 120 | 121 | const applicationsMap = applications.reduce( 122 | (all, current) => { 123 | all[current.id] = current; 124 | return all; 125 | }, 126 | {} as Record 127 | ); 128 | 129 | for (const id in results) { 130 | const calc = results[id]; 131 | const application = applicationsMap[id]; 132 | 133 | const conversionUSD = { 134 | amount: convertTokenToFiat({ 135 | tokenAmount: calc.matched, 136 | tokenDecimals: roundTokenPriceInUsd.tokenDecimals, 137 | tokenPrice: roundTokenPriceInUsd.priceInUsd, 138 | tokenPriceDecimals: 8, 139 | }), 140 | price: roundTokenPriceInUsd.priceInUsd, 141 | }; 142 | 143 | augmented.push({ 144 | ...calc, 145 | matchedUSD: conversionUSD.amount, 146 | projectId: application.projectId, 147 | applicationId: application.id, 148 | projectName: application.metadata?.application?.project?.title, 149 | payoutAddress: application.metadata?.application?.recipient, 150 | }); 151 | } 152 | 153 | return augmented; 154 | }; 155 | -------------------------------------------------------------------------------- /src/calculator/calculationConfig.ts: -------------------------------------------------------------------------------- 1 | import { DeprecatedRound } from "../deprecatedJsonDatabase.js"; 2 | 3 | export interface CalculationConfig { 4 | minimumAmountUSD?: number; 5 | enablePassport?: boolean; 6 | matchingCapAmount?: bigint; 7 | matchAmount: bigint; 8 | ignoreSaturation?: boolean; 9 | } 10 | 11 | export function overrideCalculationConfig( 12 | config: CalculationConfig, 13 | overrides: Partial 14 | ): CalculationConfig { 15 | return { 16 | minimumAmountUSD: overrides.minimumAmountUSD ?? config.minimumAmountUSD, 17 | enablePassport: overrides.enablePassport ?? config.enablePassport, 18 | matchingCapAmount: overrides.matchingCapAmount ?? config.matchingCapAmount, 19 | matchAmount: overrides.matchAmount ?? config.matchAmount, 20 | ignoreSaturation: overrides.ignoreSaturation ?? config.ignoreSaturation, 21 | }; 22 | } 23 | 24 | export function extractCalculationConfigFromRound( 25 | round: DeprecatedRound 26 | ): CalculationConfig { 27 | const matchAmount = BigInt(round.matchAmount); 28 | 29 | let matchingCapAmount: bigint | undefined = undefined; 30 | 31 | if (round.metadata?.quadraticFundingConfig?.matchingCapAmount !== undefined) { 32 | // round.metadata.quadraticFundingConfig.matchingCapAmount is a percentage, 33 | // from 0 to 100, and can have decimals 34 | const capPercentage = 35 | round.metadata?.quadraticFundingConfig?.matchingCapAmount ?? 0; 36 | 37 | // convert the capAmount to a big int (50.5% becomes 5050, 50.00005% becomes 5000) 38 | const scaledCapPercentage = BigInt(Math.trunc(Number(capPercentage) * 100)); 39 | 40 | matchingCapAmount = (matchAmount * scaledCapPercentage) / 10000n; 41 | } 42 | 43 | const sybilDefense = round?.metadata?.quadraticFundingConfig?.sybilDefense; 44 | 45 | const enablePassport = 46 | sybilDefense === undefined 47 | ? undefined 48 | : typeof sybilDefense === "boolean" 49 | ? sybilDefense 50 | : sybilDefense === "passport" 51 | ? true 52 | : false; 53 | 54 | const minimumAmountUSD = 55 | round.metadata?.quadraticFundingConfig?.minDonationThresholdAmount === 56 | undefined 57 | ? undefined 58 | : Number( 59 | round.metadata?.quadraticFundingConfig?.minDonationThresholdAmount 60 | ); 61 | 62 | return { 63 | minimumAmountUSD, 64 | enablePassport, 65 | matchAmount, 66 | matchingCapAmount, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/calculator/coefficientOverrides.ts: -------------------------------------------------------------------------------- 1 | export type CoefficientOverrides = Record; 2 | import csv from "csv-parser"; 3 | import { CalculatorError } from "./errors.js"; 4 | 5 | export class CoefficientOverridesColumnNotFoundError extends CalculatorError { 6 | constructor(column: string) { 7 | super(`cannot find column ${column} in the overrides file`); 8 | } 9 | } 10 | 11 | export class CoefficientOverridesInvalidRowError extends CalculatorError { 12 | constructor(row: number, message: string) { 13 | super(`Row ${row} in the overrides file is invalid: ${message}`); 14 | } 15 | } 16 | 17 | export function parseCoefficientOverridesCsv( 18 | buf: Buffer 19 | ): Promise { 20 | return new Promise((resolve, _reject) => { 21 | const results: CoefficientOverrides = {}; 22 | let rowIndex = 1; 23 | 24 | const stream = csv() 25 | .on("headers", (headers: string[]) => { 26 | if (headers.indexOf("id") < 0) { 27 | throw new CoefficientOverridesColumnNotFoundError("id"); 28 | } 29 | 30 | if (headers.indexOf("coefficient") < 0) { 31 | throw new CoefficientOverridesColumnNotFoundError("coefficient"); 32 | } 33 | }) 34 | .on("data", (data: Record) => { 35 | const coefficient = Number(data["coefficient"]); 36 | if (!Number.isFinite(coefficient)) { 37 | throw new CoefficientOverridesInvalidRowError( 38 | rowIndex, 39 | `Coefficient must be a number, found: ${data["coefficient"]}` 40 | ); 41 | } 42 | 43 | results[data["id"]] = coefficient; 44 | rowIndex += 1; 45 | }) 46 | .on("end", () => { 47 | resolve(results); 48 | }); 49 | 50 | stream.write(buf); 51 | stream.end(); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/calculator/dataProvider/cachedDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { DataProvider } from "./index.js"; 2 | 3 | interface CachedDataProviderOptions { 4 | dataProvider: DataProvider; 5 | cache: { 6 | set(key: string, value: unknown): void; 7 | get(key: string): unknown | undefined; 8 | }; 9 | } 10 | 11 | export class CachedDataProvider implements DataProvider { 12 | cache: CachedDataProviderOptions["cache"]; 13 | dataProvider: CachedDataProviderOptions["dataProvider"]; 14 | 15 | constructor(options: CachedDataProviderOptions) { 16 | this.cache = options.cache; 17 | this.dataProvider = options.dataProvider; 18 | } 19 | 20 | async loadFile(description: string, path: string): Promise> { 21 | const cacheKey = `data-provider:${path}`; 22 | const cachedValue = this.cache.get(cacheKey); 23 | 24 | if (cachedValue !== undefined) { 25 | return cachedValue as Array; 26 | } 27 | 28 | const value = await this.dataProvider.loadFile(description, path); 29 | this.cache.set(cacheKey, value); 30 | return value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/calculator/dataProvider/databaseDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "../../database/index.js"; 2 | import { DataProvider } from "./index.js"; 3 | import { 4 | type DeprecatedVote, 5 | type DeprecatedDetailedVote, 6 | createDeprecatedRound, 7 | createDeprecatedVote, 8 | createDeprecatedProject, 9 | createDeprecatedApplication, 10 | } from "../../deprecatedJsonDatabase.js"; 11 | import { parseAddress } from "../../address.js"; 12 | import { FileNotFoundError } from "../errors.js"; 13 | import { z } from "zod"; 14 | 15 | function parseRoundId(id: string): string { 16 | if (id.startsWith("0x")) { 17 | return parseAddress(id); 18 | } 19 | 20 | return id; 21 | } 22 | 23 | export class DatabaseDataProvider implements DataProvider { 24 | #db: Database; 25 | 26 | constructor(db: Database) { 27 | this.#db = db; 28 | } 29 | 30 | async loadFile(description: string, path: string): Promise> { 31 | const segments = path.split("/"); 32 | 33 | // /:chainId/contributors/a1/a2/a3/a4/a5/a6/a7.json 34 | if (segments.length === 9 && segments[1] === "contributors") { 35 | const chainId = Number(segments[0]); 36 | const address = parseAddress( 37 | segments 38 | .slice(2, segments.length) 39 | .map((s) => s.slice(0, 6)) 40 | .join("") 41 | ); 42 | 43 | const donations = 44 | await this.#db.getDonationsByDonorAddressWithProjectAndRound( 45 | chainId, 46 | address 47 | ); 48 | 49 | const deprecatedContributions: DeprecatedDetailedVote[] = 50 | donations.flatMap((d) => { 51 | const roundSchema = z.object({ 52 | name: z.string(), 53 | }); 54 | 55 | const projectSchema = z.object({ 56 | title: z.string(), 57 | }); 58 | 59 | const roundMetadata = roundSchema.safeParse(d.roundMetadata); 60 | const projectMetadata = projectSchema.safeParse(d.projectMetadata); 61 | 62 | if (!roundMetadata.success || !projectMetadata.success) { 63 | return []; 64 | } 65 | 66 | if (d.donationsStartTime === null || d.donationsEndTime === null) { 67 | return []; 68 | } 69 | 70 | return [ 71 | { 72 | ...createDeprecatedVote(d), 73 | roundName: roundMetadata.data.name, 74 | projectTitle: projectMetadata.data.title, 75 | roundStartTime: Math.trunc( 76 | d.donationsStartTime.getTime() / 1000 77 | ).toString(), 78 | roundEndTime: Math.trunc( 79 | d.donationsEndTime.getTime() / 1000 80 | ).toString(), 81 | }, 82 | ]; 83 | }); 84 | 85 | return deprecatedContributions as Array; 86 | } 87 | 88 | // /:chainId/rounds.json 89 | if (segments.length === 2 && segments[1] === "rounds.json") { 90 | const chainId = Number(segments[0]); 91 | 92 | const rounds = await this.#db.getAllChainRounds(chainId); 93 | 94 | const deprecatedRounds = rounds.map(createDeprecatedRound); 95 | 96 | return deprecatedRounds as unknown as Array; 97 | } 98 | 99 | // /:chainId/projects.json 100 | if (segments.length === 2 && segments[1] === "projects.json") { 101 | const chainId = Number(segments[0]); 102 | 103 | const projects = await this.#db.getAllChainProjects(chainId); 104 | const deprecatedProjects = projects.map(createDeprecatedProject); 105 | 106 | return deprecatedProjects as unknown as Array; 107 | } 108 | 109 | // /:chainId/rounds/:roundId/applications.json 110 | if ( 111 | segments.length === 4 && 112 | segments[1] === "rounds" && 113 | segments[3] === "applications.json" 114 | ) { 115 | const chainId = Number(segments[0]); 116 | const roundId = parseRoundId(segments[2]); 117 | 118 | const applications = await this.#db.getAllRoundApplications( 119 | chainId, 120 | roundId 121 | ); 122 | 123 | const deprecatedApplications = applications.map( 124 | createDeprecatedApplication 125 | ); 126 | 127 | return deprecatedApplications as unknown as Array; 128 | } 129 | 130 | // /:chainId/rounds/:roundId/votes.json 131 | if ( 132 | segments.length === 4 && 133 | segments[1] === "rounds" && 134 | segments[3] === "votes.json" 135 | ) { 136 | const chainId = Number(segments[0]); 137 | const roundId = parseRoundId(segments[2]); 138 | 139 | const donations = await this.#db.getAllRoundDonations(chainId, roundId); 140 | 141 | const votes: DeprecatedVote[] = donations.map(createDeprecatedVote); 142 | 143 | return votes as unknown as Array; 144 | } 145 | 146 | throw new FileNotFoundError(description); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/calculator/dataProvider/fileSystemDataProvider.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import { DataProvider } from "./index.js"; 3 | import { FileNotFoundError } from "../errors.js"; 4 | 5 | export class FileSystemDataProvider implements DataProvider { 6 | basePath: string; 7 | 8 | constructor(basePath: string) { 9 | this.basePath = basePath; 10 | } 11 | 12 | async loadFile(description: string, path: string): Promise> { 13 | const fullPath = `${this.basePath}/${path}`; 14 | 15 | try { 16 | const data = await fs.readFile(fullPath, "utf8"); 17 | return JSON.parse(data) as Array; 18 | } catch (err) { 19 | if (err instanceof Error && "code" in err && err.code === "ENOENT") { 20 | throw new FileNotFoundError(description); 21 | } else { 22 | throw err; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/calculator/dataProvider/index.ts: -------------------------------------------------------------------------------- 1 | export interface DataProvider { 2 | loadFile(description: string, path: string): Promise>; 3 | } 4 | -------------------------------------------------------------------------------- /src/calculator/errors.ts: -------------------------------------------------------------------------------- 1 | import ClientError from "../http/api/clientError.js"; 2 | 3 | export class CalculatorError extends ClientError { 4 | constructor(message: string, status = 400) { 5 | super(message, status); 6 | } 7 | } 8 | 9 | export class FileNotFoundError extends CalculatorError { 10 | constructor(fileDescription: string) { 11 | super(`cannot find ${fileDescription} file`, 404); 12 | } 13 | } 14 | 15 | export class ResourceNotFoundError extends CalculatorError { 16 | constructor(resource: string) { 17 | super(`${resource} not found`, 404); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/calculator/linearQf/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggregatedContributions, 3 | LinearQFOptions, 4 | linearQFWithAggregates, 5 | } from "pluralistic"; 6 | 7 | export type LinearQfCalculatorArgs = { 8 | aggregatedContributions: AggregatedContributions; 9 | matchAmount: bigint; 10 | options: LinearQFOptions; 11 | }; 12 | 13 | export type LinearQfCalculatorResult = ReturnType< 14 | typeof linearQFWithAggregates 15 | >; 16 | 17 | export type LinearQf = ( 18 | msg: LinearQfCalculatorArgs 19 | ) => Promise; 20 | -------------------------------------------------------------------------------- /src/calculator/linearQf/worker.ts: -------------------------------------------------------------------------------- 1 | import { linearQFWithAggregates } from "pluralistic"; 2 | import { parentPort } from "worker_threads"; 3 | import { LinearQfCalculatorArgs } from "./index.js"; 4 | 5 | if (parentPort === null) { 6 | throw new Error("needs to run as worker thread"); 7 | } 8 | 9 | parentPort.on("message", (msg: LinearQfCalculatorArgs) => { 10 | const result = linearQFWithAggregates( 11 | msg.aggregatedContributions, 12 | msg.matchAmount, 13 | 0n, 14 | msg.options 15 | ); 16 | 17 | parentPort!.postMessage(result); 18 | }); 19 | -------------------------------------------------------------------------------- /src/calculator/options.ts: -------------------------------------------------------------------------------- 1 | export type ProportionalMatchOptions = { 2 | score: { 3 | min: number; 4 | max: number; 5 | }; 6 | matchProportionPercentage: { 7 | min: number; 8 | max: number; 9 | }; 10 | }; 11 | 12 | export const defaultProportionalMatchOptions: ProportionalMatchOptions = { 13 | score: { 14 | min: 15, 15 | max: 25, 16 | }, 17 | matchProportionPercentage: { 18 | min: 50, 19 | max: 100, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/calculator/roundContributionsCache.ts: -------------------------------------------------------------------------------- 1 | import TTLCache from "@isaacs/ttlcache"; 2 | import { AggregatedContributions } from "pluralistic"; 3 | 4 | export interface RoundContributionsCacheKey { 5 | chainId: number; 6 | roundId: string; 7 | } 8 | 9 | export class RoundContributionsCache { 10 | private cache: TTLCache; 11 | 12 | constructor() { 13 | this.cache = new TTLCache({ 14 | ttl: 5 * 60 * 1000, // 5 minutes 15 | max: 10, // keep a maximum of 10 rounds in memory 16 | }); 17 | } 18 | 19 | async getCalculationForRound( 20 | key: RoundContributionsCacheKey 21 | ): Promise { 22 | return await this.cache.get(`${key.roundId}-${key.chainId}`); 23 | } 24 | 25 | setCalculationForRound({ 26 | roundId, 27 | chainId, 28 | contributions, 29 | }: RoundContributionsCacheKey & { 30 | contributions: AggregatedContributions; 31 | }): void { 32 | this.cache.set(`${roundId}-${chainId}`, contributions); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/contractSubscriptionPruner.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "pino"; 2 | import { Indexer } from "./indexer/indexer.js"; 3 | import { ContractName } from "./indexer/abis/index.js"; 4 | import { RpcClient } from "chainsauce/dist/rpc.js"; 5 | 6 | const CONTRACT_EXPIRATION_IN_DAYS: Partial> = { 7 | "AlloV1/RoundImplementation/V1": 60, 8 | "AlloV1/RoundImplementation/V2": 60, 9 | "AlloV1/QuadraticFundingVotingStrategyImplementation/V2": 60, 10 | "AlloV1/DirectPayoutStrategyImplementation/V2": 60, 11 | "AlloV1/MerklePayoutStrategyImplementation/V2": 60, 12 | }; 13 | 14 | export class ContractSubscriptionPruner { 15 | #client: RpcClient; 16 | #indexer: Indexer; 17 | #logger: Logger; 18 | 19 | #intervalMs = 10 * 60 * 1000; // 10 minutes 20 | 21 | #timer: NodeJS.Timeout | null = null; 22 | 23 | constructor(opts: { client: RpcClient; indexer: Indexer; logger: Logger }) { 24 | this.#client = opts.client; 25 | this.#indexer = opts.indexer; 26 | this.#logger = opts.logger; 27 | } 28 | 29 | start() { 30 | if (this.#timer !== null) { 31 | throw new Error("Pruner already started"); 32 | } 33 | 34 | void this.#prune(); 35 | } 36 | 37 | stop() { 38 | if (this.#timer === null) { 39 | throw new Error("Pruner not started"); 40 | } 41 | 42 | clearTimeout(this.#timer); 43 | this.#timer = null; 44 | } 45 | 46 | #scheduleNextPrune() { 47 | this.#timer = setTimeout(() => this.#prune(), this.#intervalMs); 48 | } 49 | 50 | async #prune(): Promise { 51 | try { 52 | const subscriptions = this.#indexer.getSubscriptions(); 53 | 54 | for (const subscription of subscriptions) { 55 | const expirationInDays = 56 | CONTRACT_EXPIRATION_IN_DAYS[ 57 | subscription.contractName as ContractName 58 | ]; 59 | 60 | if (expirationInDays === undefined) { 61 | continue; 62 | } 63 | 64 | const fromBlock = await this.#client.getBlockByNumber({ 65 | number: subscription.fromBlock, 66 | }); 67 | 68 | if (fromBlock === null) { 69 | continue; 70 | } 71 | 72 | const fromBlockDate = new Date(fromBlock.timestamp * 1000); 73 | 74 | const expirationDate = new Date( 75 | fromBlockDate.getTime() + expirationInDays * 24 * 60 * 60 * 1000 76 | ); 77 | 78 | const now = new Date(); 79 | 80 | if (expirationDate < now) { 81 | this.#logger.info({ 82 | msg: "pruning contract", 83 | contractName: subscription.contractName, 84 | address: subscription.contractAddress, 85 | }); 86 | this.#indexer.unsubscribeFromContract({ 87 | address: subscription.contractAddress, 88 | }); 89 | } 90 | } 91 | } catch (err) { 92 | this.#logger.error({ 93 | msg: "pruner error", 94 | err, 95 | }); 96 | } finally { 97 | this.#scheduleNextPrune(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/database/changeset.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Address } from "../types.js"; 2 | import { 3 | NewProject, 4 | PartialProject, 5 | NewPendingProjectRole, 6 | NewProjectRole, 7 | ProjectRole, 8 | NewRound, 9 | PartialRound, 10 | NewPendingRoundRole, 11 | NewRoundRole, 12 | RoundRole, 13 | NewApplication, 14 | PartialApplication, 15 | NewDonation, 16 | NewPrice, 17 | NewLegacyProject, 18 | NewApplicationPayout, 19 | NewIpfsData, 20 | NewAttestationData, 21 | } from "./schema.js"; 22 | 23 | export type DataChange = 24 | | { 25 | type: "InsertProject"; 26 | project: NewProject; 27 | } 28 | | { 29 | type: "UpdateProject"; 30 | chainId: ChainId; 31 | projectId: string; 32 | project: PartialProject; 33 | } 34 | | { 35 | type: "InsertPendingProjectRole"; 36 | pendingProjectRole: NewPendingProjectRole; 37 | } 38 | | { 39 | type: "DeletePendingProjectRoles"; 40 | ids: number[]; 41 | } 42 | | { 43 | type: "InsertProjectRole"; 44 | projectRole: NewProjectRole; 45 | } 46 | | { 47 | type: "DeleteAllProjectRolesByRole"; 48 | projectRole: Pick; 49 | } 50 | | { 51 | type: "DeleteAllProjectRolesByRoleAndAddress"; 52 | projectRole: Pick< 53 | ProjectRole, 54 | "chainId" | "projectId" | "role" | "address" 55 | >; 56 | } 57 | | { 58 | type: "InsertRound"; 59 | round: NewRound; 60 | } 61 | | { 62 | type: "UpdateRound"; 63 | chainId: ChainId; 64 | roundId: string; 65 | round: PartialRound; 66 | } 67 | | { 68 | type: "UpdateRoundByStrategyAddress"; 69 | chainId: ChainId; 70 | strategyAddress: Address; 71 | round: PartialRound; 72 | } 73 | | { 74 | type: "IncrementRoundFundedAmount"; 75 | chainId: ChainId; 76 | roundId: string; 77 | fundedAmount: bigint; 78 | fundedAmountInUsd: number; 79 | } 80 | | { 81 | type: "IncrementRoundDonationStats"; 82 | chainId: ChainId; 83 | roundId: Address; 84 | amountInUsd: number; 85 | } 86 | | { 87 | type: "IncrementRoundTotalDistributed"; 88 | chainId: ChainId; 89 | roundId: string; 90 | amount: bigint; 91 | } 92 | | { 93 | type: "IncrementApplicationDonationStats"; 94 | chainId: ChainId; 95 | roundId: Address; 96 | applicationId: string; 97 | amountInUsd: number; 98 | } 99 | | { 100 | type: "InsertPendingRoundRole"; 101 | pendingRoundRole: NewPendingRoundRole; 102 | } 103 | | { 104 | type: "DeletePendingRoundRoles"; 105 | ids: number[]; 106 | } 107 | | { 108 | type: "InsertRoundRole"; 109 | roundRole: NewRoundRole; 110 | } 111 | | { 112 | type: "DeleteAllRoundRolesByRoleAndAddress"; 113 | roundRole: Pick; 114 | } 115 | | { 116 | type: "InsertApplication"; 117 | application: NewApplication; 118 | } 119 | | { 120 | type: "UpdateApplication"; 121 | chainId: ChainId; 122 | roundId: string; 123 | applicationId: string; 124 | application: PartialApplication; 125 | } 126 | | { 127 | type: "InsertDonation"; 128 | donation: NewDonation; 129 | } 130 | | { 131 | type: "InsertManyDonations"; 132 | donations: NewDonation[]; 133 | } 134 | | { 135 | type: "InsertManyPrices"; 136 | prices: NewPrice[]; 137 | } 138 | | { 139 | type: "NewLegacyProject"; 140 | legacyProject: NewLegacyProject; 141 | } 142 | | { 143 | type: "InsertApplicationPayout"; 144 | payout: NewApplicationPayout; 145 | } 146 | | { 147 | type: "InsertIpfsData"; 148 | ipfs: NewIpfsData; 149 | } 150 | | { 151 | type: "InsertAttestation"; 152 | attestation: NewAttestationData; 153 | }; 154 | -------------------------------------------------------------------------------- /src/deprecatedJsonDatabase.ts: -------------------------------------------------------------------------------- 1 | import { Hex } from "./types.js"; 2 | import { Application, Donation, Project, Round } from "./database/schema.js"; 3 | import { getAddress } from "viem"; 4 | 5 | function maybeChecksumAddress(address: string): string { 6 | try { 7 | return getAddress(address); 8 | } catch (e) { 9 | return address; 10 | } 11 | } 12 | 13 | export function createDeprecatedRound(round: Round): DeprecatedRound { 14 | return { 15 | id: maybeChecksumAddress(round.id.toString()), 16 | amountUSD: round.totalAmountDonatedInUsd, 17 | votes: round.totalDonationsCount, 18 | token: getAddress(round.matchTokenAddress), 19 | matchAmount: round.matchAmount.toString(), 20 | matchAmountUSD: round.matchAmountInUsd, 21 | uniqueContributors: round.uniqueDonorsCount, 22 | applicationMetaPtr: round.applicationMetadataCid, 23 | applicationMetadata: round.applicationMetadata, 24 | metaPtr: round.roundMetadataCid, 25 | metadata: round.roundMetadata as DeprecatedRound["metadata"], 26 | applicationsStartTime: round.applicationsStartTime 27 | ? Math.trunc(round.applicationsStartTime.getTime() / 1000).toString() 28 | : "", 29 | applicationsEndTime: round.applicationsEndTime 30 | ? Math.trunc(round.applicationsEndTime.getTime() / 1000).toString() 31 | : "", 32 | roundStartTime: round.donationsStartTime 33 | ? Math.trunc(round.donationsStartTime.getTime() / 1000).toString() 34 | : "", 35 | roundEndTime: round.donationsEndTime 36 | ? Math.trunc(round.donationsEndTime.getTime() / 1000).toString() 37 | : "", 38 | createdAtBlock: Number(round.createdAtBlock), 39 | updatedAtBlock: Number(round.updatedAtBlock), 40 | }; 41 | } 42 | 43 | export function createDeprecatedVote(donation: Donation): DeprecatedVote { 44 | return { 45 | id: donation.id, 46 | transaction: donation.transactionHash, 47 | blockNumber: Number(donation.blockNumber), 48 | projectId: donation.projectId, 49 | roundId: donation.roundId, 50 | applicationId: donation.applicationId, 51 | token: getAddress(donation.tokenAddress), 52 | voter: getAddress(donation.donorAddress), 53 | grantAddress: getAddress(donation.recipientAddress), 54 | amount: donation.amount.toString(), 55 | amountUSD: donation.amountInUsd, 56 | amountRoundToken: donation.amountInRoundMatchToken.toString(), 57 | }; 58 | } 59 | 60 | export function createDeprecatedApplication( 61 | application: Application 62 | ): DeprecatedApplication { 63 | return { 64 | id: application.id, 65 | projectId: application.projectId, 66 | anchorAddress: application.anchorAddress, 67 | status: application.status, 68 | amountUSD: application.totalAmountDonatedInUsd, 69 | votes: application.totalDonationsCount, 70 | uniqueContributors: application.uniqueDonorsCount, 71 | metadata: application.metadata as DeprecatedApplication["metadata"], 72 | createdAtBlock: Number(application.createdAtBlock), 73 | statusUpdatedAtBlock: Number(application.statusUpdatedAtBlock), 74 | roundId: application.roundId, 75 | statusSnapshots: application.statusSnapshots.map((snapshot) => ({ 76 | status: snapshot.status, 77 | statusUpdatedAtBlock: Number(snapshot.updatedAtBlock), 78 | })), 79 | }; 80 | } 81 | 82 | export function createDeprecatedProject(project: Project): DeprecatedProject { 83 | return { 84 | id: project.id, 85 | metaPtr: project.metadataCid, 86 | createdAtBlock: Number(project.createdAtBlock), 87 | projectNumber: project.projectNumber, 88 | metadata: project.metadata as DeprecatedProject["metadata"], 89 | }; 90 | } 91 | 92 | export type DeprecatedRound = { 93 | id: Hex | string; 94 | amountUSD: number; 95 | votes: number; 96 | token: string; 97 | matchAmount: string; 98 | matchAmountUSD: number; 99 | uniqueContributors: number; 100 | applicationMetaPtr: string; 101 | applicationMetadata: unknown | null; 102 | metaPtr: string | null; 103 | metadata: { 104 | name: string; 105 | quadraticFundingConfig?: { 106 | matchingFundsAvailable?: number; 107 | sybilDefense?: SybilDefense | boolean; 108 | matchingCap?: boolean; 109 | matchingCapAmount?: number; 110 | minDonationThreshold?: boolean; 111 | minDonationThresholdAmount?: number; 112 | }; 113 | } | null; 114 | applicationsStartTime: string; 115 | applicationsEndTime: string; 116 | roundStartTime: string; 117 | roundEndTime: string; 118 | createdAtBlock: number; 119 | updatedAtBlock: number; 120 | }; 121 | 122 | export type SybilDefense = "passport" | "passport-mbds" | "none"; 123 | 124 | export type DeprecatedProject = { 125 | id: string; 126 | metaPtr: string | null; 127 | createdAtBlock: number; 128 | projectNumber: number | null; 129 | metadata: { 130 | title: string; 131 | description: string; 132 | website: string; 133 | projectTwitter: string; 134 | projectGithub: string; 135 | userGithub: string; 136 | logoImg: string; 137 | bannerImg: string; 138 | logoImgData: string; 139 | bannerImgData: string; 140 | cretedAt: number; 141 | } | null; 142 | }; 143 | 144 | export type DeprecatedDetailedVote = DeprecatedVote & { 145 | roundName?: string; 146 | projectTitle?: string; 147 | roundStartTime?: string; 148 | roundEndTime?: string; 149 | }; 150 | 151 | export type DeprecatedApplication = { 152 | id: string; 153 | projectId: string; 154 | roundId: string; 155 | status: "PENDING" | "APPROVED" | "REJECTED" | "CANCELLED" | "IN_REVIEW"; 156 | amountUSD: number; 157 | votes: number; 158 | uniqueContributors: number; 159 | anchorAddress: string | null; 160 | metadata: { 161 | application: { 162 | project: { 163 | title: string; 164 | website: string; 165 | projectTwitter: string; 166 | projectGithub: string; 167 | userGithub: string; 168 | }; 169 | answers: Array<{ 170 | question: string; 171 | answer: string; 172 | encryptedAnswer: string | null; 173 | }>; 174 | recipient: string; 175 | }; 176 | } | null; 177 | createdAtBlock: number; 178 | statusUpdatedAtBlock: number; 179 | statusSnapshots: Array<{ 180 | status: "PENDING" | "APPROVED" | "REJECTED" | "CANCELLED" | "IN_REVIEW"; 181 | statusUpdatedAtBlock: number; 182 | }>; 183 | }; 184 | 185 | export type DeprecatedVote = { 186 | id: string; 187 | transaction: Hex; 188 | blockNumber: number; 189 | projectId: string; 190 | roundId: string; 191 | applicationId: string; 192 | token: string; 193 | voter: string; 194 | grantAddress: string; 195 | amount: string; 196 | amountUSD: number; 197 | amountRoundToken: string; 198 | }; 199 | -------------------------------------------------------------------------------- /src/diskCache.ts: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto"; 2 | import fs from "node:fs/promises"; 3 | import path from "node:path"; 4 | 5 | // A simple disk cache that stores JSON-serializable values. 6 | // This class is deprecated and will be removed, do not use for new code. 7 | export class DeprecatedDiskCache { 8 | private dir: string; 9 | private initializePromise: Promise | null; 10 | 11 | constructor(dir: string) { 12 | this.dir = dir; 13 | this.initializePromise = null; 14 | } 15 | 16 | private filename(key: string) { 17 | const hash = crypto.createHash("sha256").update(key).digest("hex"); 18 | return path.join(this.dir, hash); 19 | } 20 | 21 | async ensureInitialized() { 22 | if (this.initializePromise === null) { 23 | this.initializePromise = fs 24 | .mkdir(this.dir, { recursive: true }) 25 | .then(() => { 26 | return; 27 | }); 28 | } 29 | return this.initializePromise; 30 | } 31 | 32 | async get(key: string): Promise { 33 | await this.ensureInitialized(); 34 | const filename = this.filename(key); 35 | 36 | let fileContents; 37 | 38 | try { 39 | fileContents = await fs.readFile(filename, "utf-8"); 40 | } catch (err) { 41 | if ( 42 | typeof err === "object" && 43 | err !== null && 44 | "code" in err && 45 | err.code === "ENOENT" 46 | ) { 47 | return null; 48 | } else { 49 | throw err; 50 | } 51 | } 52 | 53 | return JSON.parse(fileContents) as T; 54 | } 55 | 56 | async set(key: string, value: unknown) { 57 | await this.ensureInitialized(); 58 | const filename = this.filename(key); 59 | 60 | await fs.writeFile(filename, JSON.stringify(value)); 61 | } 62 | 63 | async lazy(key: string, fun: () => Promise): Promise { 64 | await this.ensureInitialized(); 65 | 66 | return this.get(key).then((cachedValue) => { 67 | if (cachedValue !== null) { 68 | return cachedValue; 69 | } else { 70 | const promise = fun(); 71 | 72 | void promise.then((value) => { 73 | return this.set(key, value); 74 | }); 75 | 76 | return promise; 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | // this module doesn't have any types 2 | declare module "express-async-errors"; 3 | 4 | declare module "diskstats" { 5 | interface InodeInfo { 6 | total: number; 7 | used: number; 8 | } 9 | 10 | interface DiskStatsInfo { 11 | total: number; 12 | used: number; 13 | inodes: InodeInfo; 14 | } 15 | 16 | function check(path: string): Promise; 17 | 18 | export = { 19 | check, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/http/api/clientError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc This class is used to create a custom error that can be used to return a message to the client. 3 | * @param message - The error message to send to the client. 4 | * @param status - The status code to send to the client. 5 | * @returns A custom error object. 6 | */ 7 | export default class ClientError extends Error { 8 | status: number; 9 | constructor(message: string, status: number) { 10 | super(message); 11 | this.status = status; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/http/api/v1/index.ts: -------------------------------------------------------------------------------- 1 | import "express-async-errors"; 2 | import express, { NextFunction, Request, Response } from "express"; 3 | import * as Sentry from "@sentry/node"; 4 | 5 | import ClientError from "../clientError.js"; 6 | import { createHandler as createExportsHandler } from "./exports.js"; 7 | import { createHandler as createMatchesHandler } from "./matches.js"; 8 | import { createHandler as createStatusHandler } from "./status.js"; 9 | import { HttpApiConfig } from "../../app.js"; 10 | 11 | export const createHandler = (config: HttpApiConfig): express.Router => { 12 | const router = express.Router(); 13 | 14 | router.use(createMatchesHandler(config)); 15 | router.use(createExportsHandler(config)); 16 | router.use(createStatusHandler(config)); 17 | 18 | // handle uncaught errors 19 | router.use( 20 | (err: Error, _req: Request, res: Response, _next: NextFunction) => { 21 | // return client errors 22 | if (err instanceof ClientError) { 23 | res.status(err.status); 24 | res.send({ error: err.message }); 25 | return; 26 | } 27 | 28 | config.logger?.error({ msg: "Unexpected exception", err }); 29 | 30 | Sentry.captureException(err); 31 | 32 | res.statusCode = 500; 33 | res.send({ 34 | error: "Internal server error", 35 | }); 36 | } 37 | ); 38 | 39 | return router; 40 | }; 41 | -------------------------------------------------------------------------------- /src/http/api/v1/status.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import { HttpApiConfig } from "../../app.js"; 4 | 5 | export const createHandler = (config: HttpApiConfig): express.Router => { 6 | const router = express.Router(); 7 | 8 | router.get("/status", (_req, res) => { 9 | res.json({ 10 | hostname: config.hostname, 11 | buildTag: config.buildTag, 12 | chainDataSchemaName: config.db.chainDataSchemaName, 13 | ipfsDataSchema: config.db.ipfsDataSchemaName, 14 | priceDataSchema: config.db.priceDataSchemaName, 15 | }); 16 | }); 17 | 18 | return router; 19 | }; 20 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v1/ProgramFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | // Manually added access control events: 3 | { 4 | anonymous: false, 5 | inputs: [ 6 | { 7 | indexed: true, 8 | internalType: "bytes32", 9 | name: "role", 10 | type: "bytes32", 11 | }, 12 | { 13 | indexed: true, 14 | internalType: "address", 15 | name: "account", 16 | type: "address", 17 | }, 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "sender", 22 | type: "address", 23 | }, 24 | ], 25 | name: "RoleGranted", 26 | type: "event", 27 | }, 28 | { 29 | anonymous: false, 30 | inputs: [ 31 | { 32 | indexed: true, 33 | internalType: "bytes32", 34 | name: "role", 35 | type: "bytes32", 36 | }, 37 | { 38 | indexed: true, 39 | internalType: "address", 40 | name: "account", 41 | type: "address", 42 | }, 43 | { 44 | indexed: true, 45 | internalType: "address", 46 | name: "sender", 47 | type: "address", 48 | }, 49 | ], 50 | name: "RoleRevoked", 51 | type: "event", 52 | }, 53 | // end access control events 54 | { 55 | anonymous: false, 56 | inputs: [ 57 | { 58 | indexed: false, 59 | internalType: "uint8", 60 | name: "version", 61 | type: "uint8", 62 | }, 63 | ], 64 | name: "Initialized", 65 | type: "event", 66 | }, 67 | { 68 | anonymous: false, 69 | inputs: [ 70 | { 71 | indexed: true, 72 | internalType: "address", 73 | name: "previousOwner", 74 | type: "address", 75 | }, 76 | { 77 | indexed: true, 78 | internalType: "address", 79 | name: "newOwner", 80 | type: "address", 81 | }, 82 | ], 83 | name: "OwnershipTransferred", 84 | type: "event", 85 | }, 86 | { 87 | anonymous: false, 88 | inputs: [ 89 | { 90 | indexed: false, 91 | internalType: "address", 92 | name: "programContractAddress", 93 | type: "address", 94 | }, 95 | ], 96 | name: "ProgramContractUpdated", 97 | type: "event", 98 | }, 99 | { 100 | anonymous: false, 101 | inputs: [ 102 | { 103 | indexed: true, 104 | internalType: "address", 105 | name: "programContractAddress", 106 | type: "address", 107 | }, 108 | { 109 | indexed: true, 110 | internalType: "address", 111 | name: "programImplementation", 112 | type: "address", 113 | }, 114 | ], 115 | name: "ProgramCreated", 116 | type: "event", 117 | }, 118 | { 119 | inputs: [ 120 | { 121 | internalType: "bytes", 122 | name: "encodedParameters", 123 | type: "bytes", 124 | }, 125 | ], 126 | name: "create", 127 | outputs: [ 128 | { 129 | internalType: "address", 130 | name: "", 131 | type: "address", 132 | }, 133 | ], 134 | stateMutability: "nonpayable", 135 | type: "function", 136 | }, 137 | { 138 | inputs: [], 139 | name: "initialize", 140 | outputs: [], 141 | stateMutability: "nonpayable", 142 | type: "function", 143 | }, 144 | { 145 | inputs: [], 146 | name: "owner", 147 | outputs: [ 148 | { 149 | internalType: "address", 150 | name: "", 151 | type: "address", 152 | }, 153 | ], 154 | stateMutability: "view", 155 | type: "function", 156 | }, 157 | { 158 | inputs: [], 159 | name: "programContract", 160 | outputs: [ 161 | { 162 | internalType: "address", 163 | name: "", 164 | type: "address", 165 | }, 166 | ], 167 | stateMutability: "view", 168 | type: "function", 169 | }, 170 | { 171 | inputs: [], 172 | name: "renounceOwnership", 173 | outputs: [], 174 | stateMutability: "nonpayable", 175 | type: "function", 176 | }, 177 | { 178 | inputs: [ 179 | { 180 | internalType: "address", 181 | name: "newOwner", 182 | type: "address", 183 | }, 184 | ], 185 | name: "transferOwnership", 186 | outputs: [], 187 | stateMutability: "nonpayable", 188 | type: "function", 189 | }, 190 | { 191 | inputs: [ 192 | { 193 | internalType: "address", 194 | name: "newProgramContract", 195 | type: "address", 196 | }, 197 | ], 198 | name: "updateProgramContract", 199 | outputs: [], 200 | stateMutability: "nonpayable", 201 | type: "function", 202 | }, 203 | ] as const; 204 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v1/ProgramImplementation.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | components: [ 20 | { internalType: "uint256", name: "protocol", type: "uint256" }, 21 | { internalType: "string", name: "pointer", type: "string" }, 22 | ], 23 | indexed: false, 24 | internalType: "struct MetaPtr", 25 | name: "oldMetaPtr", 26 | type: "tuple", 27 | }, 28 | { 29 | components: [ 30 | { internalType: "uint256", name: "protocol", type: "uint256" }, 31 | { internalType: "string", name: "pointer", type: "string" }, 32 | ], 33 | indexed: false, 34 | internalType: "struct MetaPtr", 35 | name: "newMetaPtr", 36 | type: "tuple", 37 | }, 38 | ], 39 | name: "MetaPtrUpdated", 40 | type: "event", 41 | }, 42 | { 43 | anonymous: false, 44 | inputs: [ 45 | { 46 | indexed: true, 47 | internalType: "bytes32", 48 | name: "role", 49 | type: "bytes32", 50 | }, 51 | { 52 | indexed: true, 53 | internalType: "bytes32", 54 | name: "previousAdminRole", 55 | type: "bytes32", 56 | }, 57 | { 58 | indexed: true, 59 | internalType: "bytes32", 60 | name: "newAdminRole", 61 | type: "bytes32", 62 | }, 63 | ], 64 | name: "RoleAdminChanged", 65 | type: "event", 66 | }, 67 | { 68 | anonymous: false, 69 | inputs: [ 70 | { 71 | indexed: true, 72 | internalType: "bytes32", 73 | name: "role", 74 | type: "bytes32", 75 | }, 76 | { 77 | indexed: true, 78 | internalType: "address", 79 | name: "account", 80 | type: "address", 81 | }, 82 | { 83 | indexed: true, 84 | internalType: "address", 85 | name: "sender", 86 | type: "address", 87 | }, 88 | ], 89 | name: "RoleGranted", 90 | type: "event", 91 | }, 92 | { 93 | anonymous: false, 94 | inputs: [ 95 | { 96 | indexed: true, 97 | internalType: "bytes32", 98 | name: "role", 99 | type: "bytes32", 100 | }, 101 | { 102 | indexed: true, 103 | internalType: "address", 104 | name: "account", 105 | type: "address", 106 | }, 107 | { 108 | indexed: true, 109 | internalType: "address", 110 | name: "sender", 111 | type: "address", 112 | }, 113 | ], 114 | name: "RoleRevoked", 115 | type: "event", 116 | }, 117 | { 118 | inputs: [], 119 | name: "DEFAULT_ADMIN_ROLE", 120 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 121 | stateMutability: "view", 122 | type: "function", 123 | }, 124 | { 125 | inputs: [], 126 | name: "PROGRAM_OPERATOR_ROLE", 127 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 128 | stateMutability: "view", 129 | type: "function", 130 | }, 131 | { 132 | inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], 133 | name: "getRoleAdmin", 134 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 135 | stateMutability: "view", 136 | type: "function", 137 | }, 138 | { 139 | inputs: [ 140 | { internalType: "bytes32", name: "role", type: "bytes32" }, 141 | { internalType: "uint256", name: "index", type: "uint256" }, 142 | ], 143 | name: "getRoleMember", 144 | outputs: [{ internalType: "address", name: "", type: "address" }], 145 | stateMutability: "view", 146 | type: "function", 147 | }, 148 | { 149 | inputs: [{ internalType: "bytes32", name: "role", type: "bytes32" }], 150 | name: "getRoleMemberCount", 151 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 152 | stateMutability: "view", 153 | type: "function", 154 | }, 155 | { 156 | inputs: [ 157 | { internalType: "bytes32", name: "role", type: "bytes32" }, 158 | { internalType: "address", name: "account", type: "address" }, 159 | ], 160 | name: "grantRole", 161 | outputs: [], 162 | stateMutability: "nonpayable", 163 | type: "function", 164 | }, 165 | { 166 | inputs: [ 167 | { internalType: "bytes32", name: "role", type: "bytes32" }, 168 | { internalType: "address", name: "account", type: "address" }, 169 | ], 170 | name: "hasRole", 171 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 172 | stateMutability: "view", 173 | type: "function", 174 | }, 175 | { 176 | inputs: [ 177 | { internalType: "bytes", name: "encodedParameters", type: "bytes" }, 178 | ], 179 | name: "initialize", 180 | outputs: [], 181 | stateMutability: "nonpayable", 182 | type: "function", 183 | }, 184 | { 185 | inputs: [], 186 | name: "metaPtr", 187 | outputs: [ 188 | { internalType: "uint256", name: "protocol", type: "uint256" }, 189 | { internalType: "string", name: "pointer", type: "string" }, 190 | ], 191 | stateMutability: "view", 192 | type: "function", 193 | }, 194 | { 195 | inputs: [ 196 | { internalType: "bytes32", name: "role", type: "bytes32" }, 197 | { internalType: "address", name: "account", type: "address" }, 198 | ], 199 | name: "renounceRole", 200 | outputs: [], 201 | stateMutability: "nonpayable", 202 | type: "function", 203 | }, 204 | { 205 | inputs: [ 206 | { internalType: "bytes32", name: "role", type: "bytes32" }, 207 | { internalType: "address", name: "account", type: "address" }, 208 | ], 209 | name: "revokeRole", 210 | outputs: [], 211 | stateMutability: "nonpayable", 212 | type: "function", 213 | }, 214 | { 215 | inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], 216 | name: "supportsInterface", 217 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 218 | stateMutability: "view", 219 | type: "function", 220 | }, 221 | { 222 | inputs: [ 223 | { 224 | components: [ 225 | { internalType: "uint256", name: "protocol", type: "uint256" }, 226 | { internalType: "string", name: "pointer", type: "string" }, 227 | ], 228 | internalType: "struct MetaPtr", 229 | name: "newMetaPtr", 230 | type: "tuple", 231 | }, 232 | ], 233 | name: "updateMetaPtr", 234 | outputs: [], 235 | stateMutability: "nonpayable", 236 | type: "function", 237 | }, 238 | ] as const; 239 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v1/QuadraticFundingVotingStrategyFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "previousOwner", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "address", 27 | name: "newOwner", 28 | type: "address", 29 | }, 30 | ], 31 | name: "OwnershipTransferred", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: true, 39 | internalType: "address", 40 | name: "votingContractAddress", 41 | type: "address", 42 | }, 43 | { 44 | indexed: true, 45 | internalType: "address", 46 | name: "votingImplementation", 47 | type: "address", 48 | }, 49 | ], 50 | name: "VotingContractCreated", 51 | type: "event", 52 | }, 53 | { 54 | anonymous: false, 55 | inputs: [ 56 | { 57 | indexed: false, 58 | internalType: "address", 59 | name: "votingContractAddress", 60 | type: "address", 61 | }, 62 | ], 63 | name: "VotingContractUpdated", 64 | type: "event", 65 | }, 66 | { 67 | inputs: [], 68 | name: "create", 69 | outputs: [ 70 | { 71 | internalType: "address", 72 | name: "", 73 | type: "address", 74 | }, 75 | ], 76 | stateMutability: "nonpayable", 77 | type: "function", 78 | }, 79 | { 80 | inputs: [], 81 | name: "initialize", 82 | outputs: [], 83 | stateMutability: "nonpayable", 84 | type: "function", 85 | }, 86 | { 87 | inputs: [], 88 | name: "owner", 89 | outputs: [ 90 | { 91 | internalType: "address", 92 | name: "", 93 | type: "address", 94 | }, 95 | ], 96 | stateMutability: "view", 97 | type: "function", 98 | }, 99 | { 100 | inputs: [], 101 | name: "renounceOwnership", 102 | outputs: [], 103 | stateMutability: "nonpayable", 104 | type: "function", 105 | }, 106 | { 107 | inputs: [ 108 | { 109 | internalType: "address", 110 | name: "newOwner", 111 | type: "address", 112 | }, 113 | ], 114 | name: "transferOwnership", 115 | outputs: [], 116 | stateMutability: "nonpayable", 117 | type: "function", 118 | }, 119 | { 120 | inputs: [ 121 | { 122 | internalType: "address", 123 | name: "newVotingContract", 124 | type: "address", 125 | }, 126 | ], 127 | name: "updateVotingContract", 128 | outputs: [], 129 | stateMutability: "nonpayable", 130 | type: "function", 131 | }, 132 | { 133 | inputs: [], 134 | name: "votingContract", 135 | outputs: [ 136 | { 137 | internalType: "address", 138 | name: "", 139 | type: "address", 140 | }, 141 | ], 142 | stateMutability: "view", 143 | type: "function", 144 | }, 145 | ] as const; 146 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v1/QuadraticFundingVotingStrategyImplementation.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: false, 20 | internalType: "address", 21 | name: "token", 22 | type: "address", 23 | }, 24 | { 25 | indexed: false, 26 | internalType: "uint256", 27 | name: "amount", 28 | type: "uint256", 29 | }, 30 | { 31 | indexed: true, 32 | internalType: "address", 33 | name: "voter", 34 | type: "address", 35 | }, 36 | { 37 | indexed: false, 38 | internalType: "address", 39 | name: "grantAddress", 40 | type: "address", 41 | }, 42 | { 43 | indexed: true, 44 | internalType: "bytes32", 45 | name: "projectId", 46 | type: "bytes32", 47 | }, 48 | { 49 | indexed: true, 50 | internalType: "address", 51 | name: "roundAddress", 52 | type: "address", 53 | }, 54 | ], 55 | name: "Voted", 56 | type: "event", 57 | }, 58 | { 59 | inputs: [], 60 | name: "VERSION", 61 | outputs: [ 62 | { 63 | internalType: "string", 64 | name: "", 65 | type: "string", 66 | }, 67 | ], 68 | stateMutability: "view", 69 | type: "function", 70 | }, 71 | { 72 | inputs: [], 73 | name: "init", 74 | outputs: [], 75 | stateMutability: "nonpayable", 76 | type: "function", 77 | }, 78 | { 79 | inputs: [], 80 | name: "initialize", 81 | outputs: [], 82 | stateMutability: "nonpayable", 83 | type: "function", 84 | }, 85 | { 86 | inputs: [], 87 | name: "roundAddress", 88 | outputs: [ 89 | { 90 | internalType: "address", 91 | name: "", 92 | type: "address", 93 | }, 94 | ], 95 | stateMutability: "view", 96 | type: "function", 97 | }, 98 | { 99 | inputs: [ 100 | { 101 | internalType: "bytes[]", 102 | name: "encodedVotes", 103 | type: "bytes[]", 104 | }, 105 | { 106 | internalType: "address", 107 | name: "voterAddress", 108 | type: "address", 109 | }, 110 | ], 111 | name: "vote", 112 | outputs: [], 113 | stateMutability: "payable", 114 | type: "function", 115 | }, 116 | ] as const; 117 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v1/RoundFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "previousOwner", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "address", 27 | name: "newOwner", 28 | type: "address", 29 | }, 30 | ], 31 | name: "OwnershipTransferred", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: false, 39 | internalType: "address", 40 | name: "roundAddress", 41 | type: "address", 42 | }, 43 | ], 44 | name: "RoundContractUpdated", 45 | type: "event", 46 | }, 47 | { 48 | anonymous: false, 49 | inputs: [ 50 | { 51 | indexed: true, 52 | internalType: "address", 53 | name: "roundAddress", 54 | type: "address", 55 | }, 56 | { 57 | indexed: true, 58 | internalType: "address", 59 | name: "ownedBy", 60 | type: "address", 61 | }, 62 | { 63 | indexed: true, 64 | internalType: "address", 65 | name: "roundImplementation", 66 | type: "address", 67 | }, 68 | ], 69 | name: "RoundCreated", 70 | type: "event", 71 | }, 72 | { 73 | inputs: [ 74 | { 75 | internalType: "bytes", 76 | name: "encodedParameters", 77 | type: "bytes", 78 | }, 79 | { 80 | internalType: "address", 81 | name: "ownedBy", 82 | type: "address", 83 | }, 84 | ], 85 | name: "create", 86 | outputs: [ 87 | { 88 | internalType: "address", 89 | name: "", 90 | type: "address", 91 | }, 92 | ], 93 | stateMutability: "nonpayable", 94 | type: "function", 95 | }, 96 | { 97 | inputs: [], 98 | name: "initialize", 99 | outputs: [], 100 | stateMutability: "nonpayable", 101 | type: "function", 102 | }, 103 | { 104 | inputs: [], 105 | name: "owner", 106 | outputs: [ 107 | { 108 | internalType: "address", 109 | name: "", 110 | type: "address", 111 | }, 112 | ], 113 | stateMutability: "view", 114 | type: "function", 115 | }, 116 | { 117 | inputs: [], 118 | name: "renounceOwnership", 119 | outputs: [], 120 | stateMutability: "nonpayable", 121 | type: "function", 122 | }, 123 | { 124 | inputs: [], 125 | name: "roundContract", 126 | outputs: [ 127 | { 128 | internalType: "address", 129 | name: "", 130 | type: "address", 131 | }, 132 | ], 133 | stateMutability: "view", 134 | type: "function", 135 | }, 136 | { 137 | inputs: [ 138 | { 139 | internalType: "address", 140 | name: "newOwner", 141 | type: "address", 142 | }, 143 | ], 144 | name: "transferOwnership", 145 | outputs: [], 146 | stateMutability: "nonpayable", 147 | type: "function", 148 | }, 149 | { 150 | inputs: [ 151 | { 152 | internalType: "address", 153 | name: "newRoundContract", 154 | type: "address", 155 | }, 156 | ], 157 | name: "updateRoundContract", 158 | outputs: [], 159 | stateMutability: "nonpayable", 160 | type: "function", 161 | }, 162 | ] as const; 163 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v2/DirectPayoutStrategyFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "previousOwner", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "address", 27 | name: "newOwner", 28 | type: "address", 29 | }, 30 | ], 31 | name: "OwnershipTransferred", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: true, 39 | internalType: "address", 40 | name: "payoutContractAddress", 41 | type: "address", 42 | }, 43 | { 44 | indexed: true, 45 | internalType: "address", 46 | name: "payoutImplementation", 47 | type: "address", 48 | }, 49 | ], 50 | name: "PayoutContractCreated", 51 | type: "event", 52 | }, 53 | { 54 | anonymous: false, 55 | inputs: [ 56 | { 57 | indexed: false, 58 | internalType: "address", 59 | name: "DirectPayoutStrategyAddress", 60 | type: "address", 61 | }, 62 | ], 63 | name: "PayoutImplementationUpdated", 64 | type: "event", 65 | }, 66 | { 67 | inputs: [ 68 | { 69 | internalType: "address", 70 | name: "_alloSettings", 71 | type: "address", 72 | }, 73 | { 74 | internalType: "address", 75 | name: "_vaultAddress", 76 | type: "address", 77 | }, 78 | { 79 | internalType: "uint32", 80 | name: "_roundFeePercentage", 81 | type: "uint32", 82 | }, 83 | { 84 | internalType: "address", 85 | name: "_roundFeeAddress", 86 | type: "address", 87 | }, 88 | ], 89 | name: "create", 90 | outputs: [ 91 | { 92 | internalType: "address", 93 | name: "", 94 | type: "address", 95 | }, 96 | ], 97 | stateMutability: "nonpayable", 98 | type: "function", 99 | }, 100 | { 101 | inputs: [], 102 | name: "initialize", 103 | outputs: [], 104 | stateMutability: "nonpayable", 105 | type: "function", 106 | }, 107 | { 108 | inputs: [], 109 | name: "nonce", 110 | outputs: [ 111 | { 112 | internalType: "uint256", 113 | name: "", 114 | type: "uint256", 115 | }, 116 | ], 117 | stateMutability: "view", 118 | type: "function", 119 | }, 120 | { 121 | inputs: [], 122 | name: "owner", 123 | outputs: [ 124 | { 125 | internalType: "address", 126 | name: "", 127 | type: "address", 128 | }, 129 | ], 130 | stateMutability: "view", 131 | type: "function", 132 | }, 133 | { 134 | inputs: [], 135 | name: "payoutImplementation", 136 | outputs: [ 137 | { 138 | internalType: "address payable", 139 | name: "", 140 | type: "address", 141 | }, 142 | ], 143 | stateMutability: "view", 144 | type: "function", 145 | }, 146 | { 147 | inputs: [], 148 | name: "renounceOwnership", 149 | outputs: [], 150 | stateMutability: "nonpayable", 151 | type: "function", 152 | }, 153 | { 154 | inputs: [ 155 | { 156 | internalType: "address", 157 | name: "newOwner", 158 | type: "address", 159 | }, 160 | ], 161 | name: "transferOwnership", 162 | outputs: [], 163 | stateMutability: "nonpayable", 164 | type: "function", 165 | }, 166 | { 167 | inputs: [ 168 | { 169 | internalType: "address payable", 170 | name: "newPayoutImplementation", 171 | type: "address", 172 | }, 173 | ], 174 | name: "updatePayoutImplementation", 175 | outputs: [], 176 | stateMutability: "nonpayable", 177 | type: "function", 178 | }, 179 | ] as const; 180 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v2/MerklePayoutStrategyFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "previousOwner", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "address", 27 | name: "newOwner", 28 | type: "address", 29 | }, 30 | ], 31 | name: "OwnershipTransferred", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: true, 39 | internalType: "address", 40 | name: "payoutContractAddress", 41 | type: "address", 42 | }, 43 | { 44 | indexed: true, 45 | internalType: "address", 46 | name: "payoutImplementation", 47 | type: "address", 48 | }, 49 | ], 50 | name: "PayoutContractCreated", 51 | type: "event", 52 | }, 53 | { 54 | anonymous: false, 55 | inputs: [ 56 | { 57 | indexed: false, 58 | internalType: "address", 59 | name: "merklePayoutStrategyAddress", 60 | type: "address", 61 | }, 62 | ], 63 | name: "PayoutImplementationUpdated", 64 | type: "event", 65 | }, 66 | { 67 | inputs: [], 68 | name: "create", 69 | outputs: [ 70 | { 71 | internalType: "address", 72 | name: "", 73 | type: "address", 74 | }, 75 | ], 76 | stateMutability: "nonpayable", 77 | type: "function", 78 | }, 79 | { 80 | inputs: [], 81 | name: "initialize", 82 | outputs: [], 83 | stateMutability: "nonpayable", 84 | type: "function", 85 | }, 86 | { 87 | inputs: [], 88 | name: "nonce", 89 | outputs: [ 90 | { 91 | internalType: "uint256", 92 | name: "", 93 | type: "uint256", 94 | }, 95 | ], 96 | stateMutability: "view", 97 | type: "function", 98 | }, 99 | { 100 | inputs: [], 101 | name: "owner", 102 | outputs: [ 103 | { 104 | internalType: "address", 105 | name: "", 106 | type: "address", 107 | }, 108 | ], 109 | stateMutability: "view", 110 | type: "function", 111 | }, 112 | { 113 | inputs: [], 114 | name: "payoutImplementation", 115 | outputs: [ 116 | { 117 | internalType: "address payable", 118 | name: "", 119 | type: "address", 120 | }, 121 | ], 122 | stateMutability: "view", 123 | type: "function", 124 | }, 125 | { 126 | inputs: [], 127 | name: "renounceOwnership", 128 | outputs: [], 129 | stateMutability: "nonpayable", 130 | type: "function", 131 | }, 132 | { 133 | inputs: [ 134 | { 135 | internalType: "address", 136 | name: "newOwner", 137 | type: "address", 138 | }, 139 | ], 140 | name: "transferOwnership", 141 | outputs: [], 142 | stateMutability: "nonpayable", 143 | type: "function", 144 | }, 145 | { 146 | inputs: [ 147 | { 148 | internalType: "address payable", 149 | name: "newPayoutImplementation", 150 | type: "address", 151 | }, 152 | ], 153 | name: "updatePayoutImplementation", 154 | outputs: [], 155 | stateMutability: "nonpayable", 156 | type: "function", 157 | }, 158 | ] as const; 159 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v2/QuadraticFundingVotingStrategyFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "previousOwner", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "address", 27 | name: "newOwner", 28 | type: "address", 29 | }, 30 | ], 31 | name: "OwnershipTransferred", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: true, 39 | internalType: "address", 40 | name: "votingContractAddress", 41 | type: "address", 42 | }, 43 | { 44 | indexed: true, 45 | internalType: "address", 46 | name: "votingImplementation", 47 | type: "address", 48 | }, 49 | ], 50 | name: "VotingContractCreated", 51 | type: "event", 52 | }, 53 | { 54 | anonymous: false, 55 | inputs: [ 56 | { 57 | indexed: false, 58 | internalType: "address", 59 | name: "votingContractAddress", 60 | type: "address", 61 | }, 62 | ], 63 | name: "VotingContractUpdated", 64 | type: "event", 65 | }, 66 | { 67 | inputs: [], 68 | name: "create", 69 | outputs: [ 70 | { 71 | internalType: "address", 72 | name: "", 73 | type: "address", 74 | }, 75 | ], 76 | stateMutability: "nonpayable", 77 | type: "function", 78 | }, 79 | { 80 | inputs: [], 81 | name: "initialize", 82 | outputs: [], 83 | stateMutability: "nonpayable", 84 | type: "function", 85 | }, 86 | { 87 | inputs: [], 88 | name: "owner", 89 | outputs: [ 90 | { 91 | internalType: "address", 92 | name: "", 93 | type: "address", 94 | }, 95 | ], 96 | stateMutability: "view", 97 | type: "function", 98 | }, 99 | { 100 | inputs: [], 101 | name: "renounceOwnership", 102 | outputs: [], 103 | stateMutability: "nonpayable", 104 | type: "function", 105 | }, 106 | { 107 | inputs: [ 108 | { 109 | internalType: "address", 110 | name: "newOwner", 111 | type: "address", 112 | }, 113 | ], 114 | name: "transferOwnership", 115 | outputs: [], 116 | stateMutability: "nonpayable", 117 | type: "function", 118 | }, 119 | { 120 | inputs: [ 121 | { 122 | internalType: "address", 123 | name: "newVotingContract", 124 | type: "address", 125 | }, 126 | ], 127 | name: "updateVotingContract", 128 | outputs: [], 129 | stateMutability: "nonpayable", 130 | type: "function", 131 | }, 132 | { 133 | inputs: [], 134 | name: "votingContract", 135 | outputs: [ 136 | { 137 | internalType: "address", 138 | name: "", 139 | type: "address", 140 | }, 141 | ], 142 | stateMutability: "view", 143 | type: "function", 144 | }, 145 | ] as const; 146 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v2/QuadraticFundingVotingStrategyImplementation.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: false, 20 | internalType: "address", 21 | name: "token", 22 | type: "address", 23 | }, 24 | { 25 | indexed: false, 26 | internalType: "uint256", 27 | name: "amount", 28 | type: "uint256", 29 | }, 30 | { 31 | indexed: true, 32 | internalType: "address", 33 | name: "voter", 34 | type: "address", 35 | }, 36 | { 37 | indexed: false, 38 | internalType: "address", 39 | name: "grantAddress", 40 | type: "address", 41 | }, 42 | { 43 | indexed: true, 44 | internalType: "bytes32", 45 | name: "projectId", 46 | type: "bytes32", 47 | }, 48 | { 49 | indexed: false, 50 | internalType: "uint256", 51 | name: "applicationIndex", 52 | type: "uint256", 53 | }, 54 | { 55 | indexed: true, 56 | internalType: "address", 57 | name: "roundAddress", 58 | type: "address", 59 | }, 60 | ], 61 | name: "Voted", 62 | type: "event", 63 | }, 64 | { 65 | anonymous: false, 66 | inputs: [ 67 | { 68 | indexed: false, 69 | internalType: "address", 70 | name: "token", 71 | type: "address", 72 | }, 73 | { 74 | indexed: false, 75 | internalType: "uint256", 76 | name: "amount", 77 | type: "uint256", 78 | }, 79 | { 80 | indexed: false, 81 | internalType: "address", 82 | name: "origin", 83 | type: "address", 84 | }, 85 | { 86 | indexed: true, 87 | internalType: "address", 88 | name: "voter", 89 | type: "address", 90 | }, 91 | { 92 | indexed: false, 93 | internalType: "address", 94 | name: "grantAddress", 95 | type: "address", 96 | }, 97 | { 98 | indexed: true, 99 | internalType: "bytes32", 100 | name: "projectId", 101 | type: "bytes32", 102 | }, 103 | { 104 | indexed: false, 105 | internalType: "uint256", 106 | name: "applicationIndex", 107 | type: "uint256", 108 | }, 109 | { 110 | indexed: true, 111 | internalType: "address", 112 | name: "roundAddress", 113 | type: "address", 114 | }, 115 | ], 116 | name: "Voted", 117 | type: "event", 118 | }, 119 | { 120 | inputs: [], 121 | name: "VERSION", 122 | outputs: [ 123 | { 124 | internalType: "string", 125 | name: "", 126 | type: "string", 127 | }, 128 | ], 129 | stateMutability: "view", 130 | type: "function", 131 | }, 132 | { 133 | inputs: [], 134 | name: "init", 135 | outputs: [], 136 | stateMutability: "nonpayable", 137 | type: "function", 138 | }, 139 | { 140 | inputs: [], 141 | name: "initialize", 142 | outputs: [], 143 | stateMutability: "nonpayable", 144 | type: "function", 145 | }, 146 | { 147 | inputs: [], 148 | name: "roundAddress", 149 | outputs: [ 150 | { 151 | internalType: "address", 152 | name: "", 153 | type: "address", 154 | }, 155 | ], 156 | stateMutability: "view", 157 | type: "function", 158 | }, 159 | { 160 | inputs: [ 161 | { 162 | internalType: "bytes[]", 163 | name: "encodedVotes", 164 | type: "bytes[]", 165 | }, 166 | { 167 | internalType: "address", 168 | name: "voterAddress", 169 | type: "address", 170 | }, 171 | ], 172 | name: "vote", 173 | outputs: [], 174 | stateMutability: "payable", 175 | type: "function", 176 | }, 177 | ] as const; 178 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v1/v2/RoundFactory.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: false, 7 | internalType: "uint8", 8 | name: "version", 9 | type: "uint8", 10 | }, 11 | ], 12 | name: "Initialized", 13 | type: "event", 14 | }, 15 | { 16 | anonymous: false, 17 | inputs: [ 18 | { 19 | indexed: true, 20 | internalType: "address", 21 | name: "previousOwner", 22 | type: "address", 23 | }, 24 | { 25 | indexed: true, 26 | internalType: "address", 27 | name: "newOwner", 28 | type: "address", 29 | }, 30 | ], 31 | name: "OwnershipTransferred", 32 | type: "event", 33 | }, 34 | { 35 | anonymous: false, 36 | inputs: [ 37 | { 38 | indexed: false, 39 | internalType: "address", 40 | name: "roundAddress", 41 | type: "address", 42 | }, 43 | ], 44 | name: "RoundContractUpdated", 45 | type: "event", 46 | }, 47 | { 48 | anonymous: false, 49 | inputs: [ 50 | { 51 | indexed: true, 52 | internalType: "address", 53 | name: "roundAddress", 54 | type: "address", 55 | }, 56 | { 57 | indexed: true, 58 | internalType: "address", 59 | name: "ownedBy", 60 | type: "address", 61 | }, 62 | { 63 | indexed: true, 64 | internalType: "address", 65 | name: "roundImplementation", 66 | type: "address", 67 | }, 68 | ], 69 | name: "RoundCreated", 70 | type: "event", 71 | }, 72 | { 73 | inputs: [ 74 | { 75 | internalType: "bytes", 76 | name: "encodedParameters", 77 | type: "bytes", 78 | }, 79 | { 80 | internalType: "address", 81 | name: "ownedBy", 82 | type: "address", 83 | }, 84 | ], 85 | name: "create", 86 | outputs: [ 87 | { 88 | internalType: "address", 89 | name: "", 90 | type: "address", 91 | }, 92 | ], 93 | stateMutability: "nonpayable", 94 | type: "function", 95 | }, 96 | { 97 | inputs: [], 98 | name: "initialize", 99 | outputs: [], 100 | stateMutability: "nonpayable", 101 | type: "function", 102 | }, 103 | { 104 | inputs: [], 105 | name: "owner", 106 | outputs: [ 107 | { 108 | internalType: "address", 109 | name: "", 110 | type: "address", 111 | }, 112 | ], 113 | stateMutability: "view", 114 | type: "function", 115 | }, 116 | { 117 | inputs: [], 118 | name: "renounceOwnership", 119 | outputs: [], 120 | stateMutability: "nonpayable", 121 | type: "function", 122 | }, 123 | { 124 | inputs: [], 125 | name: "roundContract", 126 | outputs: [ 127 | { 128 | internalType: "address", 129 | name: "", 130 | type: "address", 131 | }, 132 | ], 133 | stateMutability: "view", 134 | type: "function", 135 | }, 136 | { 137 | inputs: [ 138 | { 139 | internalType: "address", 140 | name: "newOwner", 141 | type: "address", 142 | }, 143 | ], 144 | name: "transferOwnership", 145 | outputs: [], 146 | stateMutability: "nonpayable", 147 | type: "function", 148 | }, 149 | { 150 | inputs: [ 151 | { 152 | internalType: "address", 153 | name: "newRoundContract", 154 | type: "address", 155 | }, 156 | ], 157 | name: "updateRoundContract", 158 | outputs: [], 159 | stateMutability: "nonpayable", 160 | type: "function", 161 | }, 162 | ] as const; 163 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v2/AlloV1ToV2ProfileMigration.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | inputs: [], 4 | stateMutability: "nonpayable", 5 | type: "constructor", 6 | }, 7 | { 8 | anonymous: false, 9 | inputs: [ 10 | { 11 | indexed: true, 12 | internalType: "bytes32", 13 | name: "alloV1", 14 | type: "bytes32", 15 | }, 16 | { 17 | indexed: false, 18 | internalType: "uint256", 19 | name: "alloV1ChainId", 20 | type: "uint256", 21 | }, 22 | { 23 | indexed: true, 24 | internalType: "bytes32", 25 | name: "alloV2", 26 | type: "bytes32", 27 | }, 28 | { 29 | indexed: false, 30 | internalType: "uint256", 31 | name: "nonce", 32 | type: "uint256", 33 | }, 34 | ], 35 | name: "ProfileMigrated", 36 | type: "event", 37 | }, 38 | { 39 | inputs: [ 40 | { 41 | internalType: "bytes32", 42 | name: "", 43 | type: "bytes32", 44 | }, 45 | ], 46 | name: "alloV1ToAlloV1V2Profile", 47 | outputs: [ 48 | { 49 | internalType: "bytes32", 50 | name: "alloV1", 51 | type: "bytes32", 52 | }, 53 | { 54 | internalType: "uint256", 55 | name: "alloV1ChainId", 56 | type: "uint256", 57 | }, 58 | { 59 | internalType: "bytes32", 60 | name: "alloV2", 61 | type: "bytes32", 62 | }, 63 | { 64 | internalType: "uint256", 65 | name: "nonce", 66 | type: "uint256", 67 | }, 68 | ], 69 | stateMutability: "view", 70 | type: "function", 71 | }, 72 | { 73 | inputs: [ 74 | { 75 | internalType: "bytes32", 76 | name: "", 77 | type: "bytes32", 78 | }, 79 | ], 80 | name: "alloV2ToAlloV1V2Profile", 81 | outputs: [ 82 | { 83 | internalType: "bytes32", 84 | name: "alloV1", 85 | type: "bytes32", 86 | }, 87 | { 88 | internalType: "uint256", 89 | name: "alloV1ChainId", 90 | type: "uint256", 91 | }, 92 | { 93 | internalType: "bytes32", 94 | name: "alloV2", 95 | type: "bytes32", 96 | }, 97 | { 98 | internalType: "uint256", 99 | name: "nonce", 100 | type: "uint256", 101 | }, 102 | ], 103 | stateMutability: "view", 104 | type: "function", 105 | }, 106 | { 107 | inputs: [ 108 | { 109 | internalType: "address", 110 | name: "_registry", 111 | type: "address", 112 | }, 113 | { 114 | internalType: "bytes", 115 | name: "_encodedData", 116 | type: "bytes", 117 | }, 118 | ], 119 | name: "createProfiles", 120 | outputs: [ 121 | { 122 | internalType: "bytes", 123 | name: "", 124 | type: "bytes", 125 | }, 126 | ], 127 | stateMutability: "nonpayable", 128 | type: "function", 129 | }, 130 | ] as const; 131 | -------------------------------------------------------------------------------- /src/indexer/abis/allo-v2/v1/IStrategy.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: "function", 4 | name: "allocate", 5 | inputs: [ 6 | { name: "_data", type: "bytes", internalType: "bytes" }, 7 | { name: "_sender", type: "address", internalType: "address" }, 8 | ], 9 | outputs: [], 10 | stateMutability: "payable", 11 | }, 12 | { 13 | type: "function", 14 | name: "distribute", 15 | inputs: [ 16 | { name: "_recipientIds", type: "address[]", internalType: "address[]" }, 17 | { name: "_data", type: "bytes", internalType: "bytes" }, 18 | { name: "_sender", type: "address", internalType: "address" }, 19 | ], 20 | outputs: [], 21 | stateMutability: "nonpayable", 22 | }, 23 | { 24 | type: "function", 25 | name: "getAllo", 26 | inputs: [], 27 | outputs: [{ name: "", type: "address", internalType: "contract IAllo" }], 28 | stateMutability: "view", 29 | }, 30 | { 31 | type: "function", 32 | name: "getPayouts", 33 | inputs: [ 34 | { name: "_recipientIds", type: "address[]", internalType: "address[]" }, 35 | { name: "_data", type: "bytes[]", internalType: "bytes[]" }, 36 | ], 37 | outputs: [ 38 | { 39 | name: "", 40 | type: "tuple[]", 41 | internalType: "struct IStrategy.PayoutSummary[]", 42 | components: [ 43 | { 44 | name: "recipientAddress", 45 | type: "address", 46 | internalType: "address", 47 | }, 48 | { name: "amount", type: "uint256", internalType: "uint256" }, 49 | ], 50 | }, 51 | ], 52 | stateMutability: "view", 53 | }, 54 | { 55 | type: "function", 56 | name: "getPoolAmount", 57 | inputs: [], 58 | outputs: [{ name: "", type: "uint256", internalType: "uint256" }], 59 | stateMutability: "view", 60 | }, 61 | { 62 | type: "function", 63 | name: "getPoolId", 64 | inputs: [], 65 | outputs: [{ name: "", type: "uint256", internalType: "uint256" }], 66 | stateMutability: "view", 67 | }, 68 | { 69 | type: "function", 70 | name: "getRecipientStatus", 71 | inputs: [ 72 | { name: "_recipientId", type: "address", internalType: "address" }, 73 | ], 74 | outputs: [ 75 | { name: "", type: "uint8", internalType: "enum IStrategy.Status" }, 76 | ], 77 | stateMutability: "view", 78 | }, 79 | { 80 | type: "function", 81 | name: "getStrategyId", 82 | inputs: [], 83 | outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], 84 | stateMutability: "view", 85 | }, 86 | { 87 | type: "function", 88 | name: "increasePoolAmount", 89 | inputs: [{ name: "_amount", type: "uint256", internalType: "uint256" }], 90 | outputs: [], 91 | stateMutability: "nonpayable", 92 | }, 93 | { 94 | type: "function", 95 | name: "initialize", 96 | inputs: [ 97 | { name: "_poolId", type: "uint256", internalType: "uint256" }, 98 | { name: "_data", type: "bytes", internalType: "bytes" }, 99 | ], 100 | outputs: [], 101 | stateMutability: "nonpayable", 102 | }, 103 | { 104 | type: "function", 105 | name: "isPoolActive", 106 | inputs: [], 107 | outputs: [{ name: "", type: "bool", internalType: "bool" }], 108 | stateMutability: "nonpayable", 109 | }, 110 | { 111 | type: "function", 112 | name: "isValidAllocator", 113 | inputs: [{ name: "_allocator", type: "address", internalType: "address" }], 114 | outputs: [{ name: "", type: "bool", internalType: "bool" }], 115 | stateMutability: "view", 116 | }, 117 | { 118 | type: "function", 119 | name: "registerRecipient", 120 | inputs: [ 121 | { name: "_data", type: "bytes", internalType: "bytes" }, 122 | { name: "_sender", type: "address", internalType: "address" }, 123 | ], 124 | outputs: [{ name: "", type: "address", internalType: "address" }], 125 | stateMutability: "payable", 126 | }, 127 | { 128 | type: "event", 129 | name: "Allocated", 130 | inputs: [ 131 | { 132 | name: "recipientId", 133 | type: "address", 134 | indexed: true, 135 | internalType: "address", 136 | }, 137 | { 138 | name: "amount", 139 | type: "uint256", 140 | indexed: false, 141 | internalType: "uint256", 142 | }, 143 | { 144 | name: "token", 145 | type: "address", 146 | indexed: false, 147 | internalType: "address", 148 | }, 149 | { 150 | name: "sender", 151 | type: "address", 152 | indexed: false, 153 | internalType: "address", 154 | }, 155 | ], 156 | anonymous: false, 157 | }, 158 | { 159 | type: "event", 160 | name: "Distributed", 161 | inputs: [ 162 | { 163 | name: "recipientId", 164 | type: "address", 165 | indexed: true, 166 | internalType: "address", 167 | }, 168 | { 169 | name: "recipientAddress", 170 | type: "address", 171 | indexed: false, 172 | internalType: "address", 173 | }, 174 | { 175 | name: "amount", 176 | type: "uint256", 177 | indexed: false, 178 | internalType: "uint256", 179 | }, 180 | { 181 | name: "sender", 182 | type: "address", 183 | indexed: false, 184 | internalType: "address", 185 | }, 186 | ], 187 | anonymous: false, 188 | }, 189 | { 190 | type: "event", 191 | name: "Initialized", 192 | inputs: [ 193 | { 194 | name: "poolId", 195 | type: "uint256", 196 | indexed: false, 197 | internalType: "uint256", 198 | }, 199 | { name: "data", type: "bytes", indexed: false, internalType: "bytes" }, 200 | ], 201 | anonymous: false, 202 | }, 203 | { 204 | type: "event", 205 | name: "PoolActive", 206 | inputs: [ 207 | { name: "active", type: "bool", indexed: false, internalType: "bool" }, 208 | ], 209 | anonymous: false, 210 | }, 211 | { 212 | type: "event", 213 | name: "Registered", 214 | inputs: [ 215 | { 216 | name: "recipientId", 217 | type: "address", 218 | indexed: true, 219 | internalType: "address", 220 | }, 221 | { name: "data", type: "bytes", indexed: false, internalType: "bytes" }, 222 | { 223 | name: "sender", 224 | type: "address", 225 | indexed: false, 226 | internalType: "address", 227 | }, 228 | ], 229 | anonymous: false, 230 | }, 231 | ] as const; 232 | -------------------------------------------------------------------------------- /src/indexer/abis/index.ts: -------------------------------------------------------------------------------- 1 | // V1.1 2 | import ProjectRegistryV1 from "./allo-v1/v1/ProjectRegistry.js"; 3 | import RoundFactoryV1 from "./allo-v1/v1/RoundFactory.js"; 4 | import RoundImplementationV1 from "./allo-v1/v1/RoundImplementation.js"; 5 | import QuadraticFundingVotingStrategyFactoryV1 from "./allo-v1/v1/QuadraticFundingVotingStrategyFactory.js"; 6 | import QuadraticFundingVotingStrategyImplementationV1 from "./allo-v1/v1/QuadraticFundingVotingStrategyImplementation.js"; 7 | import ProgramFactoryV1 from "./allo-v1/v1/ProgramFactory.js"; 8 | import ProgramImplementationV1 from "./allo-v1/v1/ProgramImplementation.js"; 9 | 10 | // V1.2 11 | import ProjectRegistryV2 from "./allo-v1/v2/ProjectRegistry.js"; 12 | import RoundFactoryV2 from "./allo-v1/v2/RoundFactory.js"; 13 | import RoundImplementationV2 from "./allo-v1/v2/RoundImplementation.js"; 14 | import QuadraticFundingVotingStrategyFactoryV2 from "./allo-v1/v2/QuadraticFundingVotingStrategyFactory.js"; 15 | import QuadraticFundingVotingStrategyImplementationV2 from "./allo-v1/v2/QuadraticFundingVotingStrategyImplementation.js"; 16 | import DirectPayoutStrategyFactoryV2 from "./allo-v1/v2/DirectPayoutStrategyFactory.js"; 17 | import DirectPayoutStrategyImplementationV2 from "./allo-v1/v2/DirectPayoutStrategyImplementation.js"; 18 | import MerklePayoutStrategyFactory from "./allo-v1/v2/MerklePayoutStrategyFactory.js"; 19 | import MerklePayoutStrategyImplementation from "./allo-v1/v2/MerklePayoutStrategyImplementation.js"; 20 | 21 | // V2 22 | import AlloV2 from "./allo-v2/v1/Allo.js"; 23 | import AlloV2Registry from "./allo-v2/v1/Registry.js"; 24 | import AlloV2IStrategy from "./allo-v2/v1/IStrategy.js"; 25 | import AlloV2DonationVotingMerkleDistributionDirectTransferStrategy from "./allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js"; 26 | import AlloV2DirectGrantsSimpleStrategy from "./allo-v2/v1/DirectGrantsSimpleStrategy.js"; 27 | import AlloV1ToV2ProfileMigration from "./allo-v2/AlloV1ToV2ProfileMigration.js"; 28 | import AlloV2DirectGrantsLiteStrategy from "./allo-v2/v1/DirectGrantsLiteStrategy.js"; 29 | import AlloV2EasyRPGFStrategy from "./allo-v2/v1/EasyRPGFStrategy.js"; 30 | import AlloV2DirectAllocationStrategy from "./allo-v2/v1/DirectAllocationStrategy.js"; 31 | import AlloV2EasyRetroFundingStrategy from "./allo-v2/v1/EasyRetroFundingStrategy.js"; 32 | 33 | // Gitcoin Attestation Network 34 | import GitcoinAttestationNetwork from "./gitcoin-attestation-network/GitcoinGrantsResolver.js"; 35 | 36 | const abis = { 37 | // Allo V1 38 | "AlloV1/ProjectRegistry/V1": ProjectRegistryV1, 39 | "AlloV1/ProjectRegistry/V2": ProjectRegistryV2, 40 | "AlloV1/ProgramFactory/V1": ProgramFactoryV1, 41 | "AlloV1/ProgramImplementation/V1": ProgramImplementationV1, 42 | "AlloV1/RoundFactory/V1": RoundFactoryV1, 43 | "AlloV1/RoundImplementation/V1": RoundImplementationV1, 44 | "AlloV1/QuadraticFundingVotingStrategyFactory/V1": 45 | QuadraticFundingVotingStrategyFactoryV1, 46 | "AlloV1/QuadraticFundingVotingStrategyImplementation/V1": 47 | QuadraticFundingVotingStrategyImplementationV1, 48 | "AlloV1/RoundFactory/V2": RoundFactoryV2, 49 | "AlloV1/RoundImplementation/V2": RoundImplementationV2, 50 | "AlloV1/QuadraticFundingVotingStrategyFactory/V2": 51 | QuadraticFundingVotingStrategyFactoryV2, 52 | "AlloV1/QuadraticFundingVotingStrategyImplementation/V2": 53 | QuadraticFundingVotingStrategyImplementationV2, 54 | "AlloV1/DirectPayoutStrategyFactory/V2": DirectPayoutStrategyFactoryV2, 55 | "AlloV1/DirectPayoutStrategyImplementation/V2": 56 | DirectPayoutStrategyImplementationV2, 57 | "AlloV1/MerklePayoutStrategyFactory/V2": MerklePayoutStrategyFactory, 58 | "AlloV1/MerklePayoutStrategyImplementation/V2": 59 | MerklePayoutStrategyImplementation, 60 | 61 | // Allo V2 Profile Migration 62 | "AlloV2/AlloV1ToV2ProfileMigration": AlloV1ToV2ProfileMigration, 63 | 64 | // Allo V2 Registry 65 | "AlloV2/Registry/V1": AlloV2Registry, 66 | 67 | // Allo V2 Core 68 | "AlloV2/Allo/V1": AlloV2, 69 | "AlloV2/IStrategy/V1": AlloV2IStrategy, 70 | "AlloV2/DonationVotingMerkleDistributionDirectTransferStrategy/V1": 71 | AlloV2DonationVotingMerkleDistributionDirectTransferStrategy, 72 | "AlloV2/DirectGrantsSimpleStrategy/V1": AlloV2DirectGrantsSimpleStrategy, 73 | "AlloV2/DirectGrantsLiteStrategy/V1": AlloV2DirectGrantsLiteStrategy, 74 | "AlloV2/EasyRPGFStrategy/V1": AlloV2EasyRPGFStrategy, 75 | "AlloV2/DirectAllocationStrategy/V1": AlloV2DirectAllocationStrategy, 76 | "AlloV2/EasyRetroFundingStrategy/V1": AlloV2EasyRetroFundingStrategy, 77 | // Gitcoin Attestation Network 78 | "GitcoinAttestationNetwork/GitcoinGrantsResolver": GitcoinAttestationNetwork, 79 | } as const; 80 | 81 | export default abis; 82 | export type ContractName = keyof typeof abis; 83 | -------------------------------------------------------------------------------- /src/indexer/allo/application.ts: -------------------------------------------------------------------------------- 1 | import { Block } from "chainsauce"; 2 | import { Application } from "../../database/schema.js"; 3 | 4 | export async function updateApplicationStatus( 5 | application: Application, 6 | newStatus: Application["status"], 7 | blockNumber: bigint, 8 | getBlock: () => Promise 9 | ): Promise< 10 | Pick 11 | > { 12 | const statusSnapshots = [...application.statusSnapshots]; 13 | 14 | if (application.status !== newStatus) { 15 | const block = await getBlock(); 16 | 17 | statusSnapshots.push({ 18 | status: newStatus, 19 | updatedAtBlock: blockNumber.toString(), 20 | updatedAt: new Date(block.timestamp * 1000), 21 | }); 22 | } 23 | 24 | return { 25 | status: newStatus, 26 | statusUpdatedAtBlock: blockNumber, 27 | statusSnapshots: statusSnapshots, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/indexer/allo/roundMetadata.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const RoundMetadataSchema = z 4 | .object({ 5 | name: z.string(), 6 | roundType: z.union([z.literal("private"), z.literal("public")]), 7 | quadraticFundingConfig: z.object({ 8 | matchingFundsAvailable: z.number(), 9 | }), 10 | }) 11 | .passthrough(); 12 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/applicationMetaPtrUpdated.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import { parseAddress } from "../../../address.js"; 3 | import { Changeset } from "../../../database/index.js"; 4 | import { Round } from "../../../database/schema.js"; 5 | 6 | import type { Indexer } from "../../indexer.js"; 7 | 8 | export default async function ({ 9 | event, 10 | context: { chainId, ipfsGet }, 11 | }: EventHandlerArgs< 12 | Indexer, 13 | "AlloV1/RoundImplementation/V2", 14 | "ApplicationMetaPtrUpdated" 15 | >): Promise { 16 | const id = parseAddress(event.address); 17 | const metaPtr = event.params.newMetaPtr.pointer; 18 | const metadata = await ipfsGet(metaPtr); 19 | 20 | return [ 21 | { 22 | type: "UpdateRound", 23 | roundId: id, 24 | chainId, 25 | round: { 26 | applicationMetadataCid: event.params.newMetaPtr.pointer, 27 | updatedAtBlock: event.blockNumber, 28 | applicationMetadata: metadata ?? null, 29 | }, 30 | }, 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/matchAmountUpdated.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | 3 | import type { Indexer } from "../../indexer.js"; 4 | import { Changeset } from "../../../database/index.js"; 5 | import { NewRound, Round } from "../../../database/schema.js"; 6 | 7 | import { PriceProvider, convertToUSD } from "../../../prices/provider.js"; 8 | 9 | export async function updateRoundMatchAmount(args: { 10 | round: Round | NewRound; 11 | priceProvider: PriceProvider; 12 | blockNumber: bigint; 13 | newMatchAmount: bigint; 14 | }): Promise { 15 | const { round, blockNumber, newMatchAmount, priceProvider } = args; 16 | 17 | const amountUSD = await convertToUSD( 18 | priceProvider, 19 | round.chainId, 20 | round.matchTokenAddress, 21 | newMatchAmount, 22 | blockNumber 23 | ); 24 | 25 | return { 26 | type: "UpdateRound", 27 | roundId: round.id, 28 | chainId: round.chainId, 29 | round: { 30 | updatedAtBlock: blockNumber, 31 | matchAmount: newMatchAmount, 32 | matchAmountInUsd: amountUSD.amount, 33 | }, 34 | }; 35 | } 36 | 37 | export default async function ({ 38 | event, 39 | context: { chainId, priceProvider, db }, 40 | }: EventHandlerArgs< 41 | Indexer, 42 | "AlloV1/RoundImplementation/V2", 43 | "MatchAmountUpdated" 44 | >) { 45 | const id = event.address; 46 | const matchAmount = event.params.newAmount; 47 | 48 | const round = await db.getRoundById(chainId, id); 49 | 50 | if (round === null) { 51 | throw new Error(`Round ${id} not found`); 52 | } 53 | 54 | return [ 55 | await updateRoundMatchAmount({ 56 | round, 57 | blockNumber: event.blockNumber, 58 | newMatchAmount: matchAmount, 59 | priceProvider, 60 | }), 61 | ]; 62 | } 63 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/roleGranted.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import type { Indexer } from "../../indexer.js"; 3 | import { Changeset } from "../../../database/index.js"; 4 | import { parseAddress } from "../../../address.js"; 5 | import { 6 | PROGRAM_ADMIN_ROLE, 7 | PROGRAM_OPERATOR_ROLE, 8 | ROUND_ADMIN_ROLE, 9 | ROUND_OPERATOR_ROLE, 10 | } from "./roles.js"; 11 | 12 | export default async function handleEvent( 13 | args: EventHandlerArgs< 14 | Indexer, 15 | | "AlloV1/ProgramImplementation/V1" 16 | | "AlloV1/RoundImplementation/V1" 17 | | "AlloV1/RoundImplementation/V2", 18 | "RoleGranted" 19 | > 20 | ): Promise { 21 | const { 22 | chainId, 23 | event, 24 | context: { db, logger }, 25 | } = args; 26 | 27 | switch (args.event.contractName) { 28 | case "AlloV1/ProgramImplementation/V1": { 29 | const programAddress = parseAddress(event.address); 30 | const project = await db.getProjectById(chainId, programAddress); 31 | if (project === null) { 32 | logger.warn({ 33 | msg: `Program/Project ${programAddress} not found`, 34 | event, 35 | }); 36 | return []; 37 | } 38 | 39 | const account = parseAddress(event.params.account); 40 | const role = event.params.role.toLocaleLowerCase(); 41 | 42 | switch (role) { 43 | case PROGRAM_ADMIN_ROLE: { 44 | return [ 45 | { 46 | type: "InsertProjectRole", 47 | projectRole: { 48 | chainId, 49 | projectId: programAddress, 50 | address: account, 51 | role: "owner", 52 | createdAtBlock: event.blockNumber, 53 | }, 54 | }, 55 | ]; 56 | } 57 | 58 | case PROGRAM_OPERATOR_ROLE: { 59 | return [ 60 | { 61 | type: "InsertProjectRole", 62 | projectRole: { 63 | chainId, 64 | projectId: programAddress, 65 | address: account, 66 | role: "member", 67 | createdAtBlock: event.blockNumber, 68 | }, 69 | }, 70 | ]; 71 | } 72 | 73 | default: { 74 | logger.warn({ 75 | msg: `Unknown role ${role} for program ${programAddress}`, 76 | event, 77 | }); 78 | return []; 79 | } 80 | } 81 | } 82 | 83 | case "AlloV1/RoundImplementation/V1": 84 | case "AlloV1/RoundImplementation/V2": { 85 | const roundAddress = parseAddress(event.address); 86 | const round = await db.getRoundById(chainId, roundAddress); 87 | if (round === null) { 88 | logger.warn({ 89 | msg: `Round ${chainId}/${roundAddress} not found`, 90 | event, 91 | }); 92 | return []; 93 | } 94 | 95 | const account = parseAddress(event.params.account); 96 | const role = event.params.role.toLocaleLowerCase(); 97 | 98 | switch (role) { 99 | case ROUND_ADMIN_ROLE: { 100 | return [ 101 | { 102 | type: "InsertRoundRole", 103 | roundRole: { 104 | chainId, 105 | roundId: roundAddress, 106 | address: account, 107 | role: "admin", 108 | createdAtBlock: event.blockNumber, 109 | }, 110 | }, 111 | ]; 112 | } 113 | 114 | case ROUND_OPERATOR_ROLE: { 115 | return [ 116 | { 117 | type: "InsertRoundRole", 118 | roundRole: { 119 | chainId, 120 | roundId: roundAddress, 121 | address: account, 122 | role: "manager", 123 | createdAtBlock: event.blockNumber, 124 | }, 125 | }, 126 | ]; 127 | } 128 | 129 | default: { 130 | logger.warn({ 131 | msg: `Unknown role ${role} for round ${roundAddress}`, 132 | event, 133 | }); 134 | return []; 135 | } 136 | } 137 | } 138 | 139 | default: { 140 | return []; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/roleRevoked.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import type { Indexer } from "../../indexer.js"; 3 | import { Changeset } from "../../../database/index.js"; 4 | import { parseAddress } from "../../../address.js"; 5 | import { 6 | PROGRAM_ADMIN_ROLE, 7 | PROGRAM_OPERATOR_ROLE, 8 | ROUND_ADMIN_ROLE, 9 | ROUND_OPERATOR_ROLE, 10 | } from "./roles.js"; 11 | 12 | export default async function handleEvent( 13 | args: EventHandlerArgs< 14 | Indexer, 15 | "AlloV1/ProgramImplementation/V1" | "AlloV2/Registry/V1", 16 | "RoleRevoked" 17 | > 18 | ): Promise { 19 | const { 20 | chainId, 21 | event, 22 | context: { db, logger }, 23 | } = args; 24 | 25 | switch (args.event.contractName) { 26 | case "AlloV1/ProgramImplementation/V1": { 27 | const programAddress = parseAddress(event.address); 28 | const project = await db.getProjectById(chainId, programAddress); 29 | if (project === null) { 30 | logger.warn({ 31 | msg: `Program/Project ${programAddress} not found`, 32 | event, 33 | }); 34 | return []; 35 | } 36 | 37 | const account = parseAddress(event.params.account); 38 | const role = event.params.role.toLocaleLowerCase(); 39 | 40 | switch (role) { 41 | case PROGRAM_ADMIN_ROLE: { 42 | return [ 43 | { 44 | type: "DeleteAllProjectRolesByRoleAndAddress", 45 | projectRole: { 46 | chainId, 47 | projectId: programAddress, 48 | role: "owner", 49 | address: account, 50 | }, 51 | }, 52 | ]; 53 | } 54 | 55 | case PROGRAM_OPERATOR_ROLE: { 56 | return [ 57 | { 58 | type: "DeleteAllProjectRolesByRoleAndAddress", 59 | projectRole: { 60 | chainId, 61 | projectId: programAddress, 62 | role: "member", 63 | address: account, 64 | }, 65 | }, 66 | ]; 67 | } 68 | 69 | default: { 70 | logger.warn({ 71 | msg: `Unknown role ${role} for program ${programAddress}`, 72 | event, 73 | }); 74 | return []; 75 | } 76 | } 77 | } 78 | 79 | case "AlloV1/RoundImplementation/V1": 80 | case "AlloV1/RoundImplementation/V2": { 81 | const roundAddress = parseAddress(event.address); 82 | const round = await db.getRoundById(chainId, roundAddress); 83 | if (round === null) { 84 | logger.warn({ 85 | msg: `Round ${roundAddress} not found`, 86 | event, 87 | }); 88 | return []; 89 | } 90 | 91 | const account = parseAddress(event.params.account); 92 | const role = event.params.role.toLocaleLowerCase(); 93 | 94 | switch (role) { 95 | case ROUND_ADMIN_ROLE: { 96 | return [ 97 | { 98 | type: "DeleteAllRoundRolesByRoleAndAddress", 99 | roundRole: { 100 | chainId, 101 | roundId: roundAddress, 102 | role: "admin", 103 | address: account, 104 | }, 105 | }, 106 | ]; 107 | } 108 | 109 | case ROUND_OPERATOR_ROLE: { 110 | return [ 111 | { 112 | type: "DeleteAllRoundRolesByRoleAndAddress", 113 | roundRole: { 114 | chainId, 115 | roundId: roundAddress, 116 | role: "manager", 117 | address: account, 118 | }, 119 | }, 120 | ]; 121 | } 122 | 123 | default: { 124 | logger.warn({ 125 | msg: `Unknown role ${role} for program ${roundAddress}`, 126 | event, 127 | }); 128 | return []; 129 | } 130 | } 131 | } 132 | 133 | default: { 134 | return []; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/roles.ts: -------------------------------------------------------------------------------- 1 | export const PROGRAM_ADMIN_ROLE = 2 | "0x0000000000000000000000000000000000000000000000000000000000000000"; 3 | 4 | export const PROGRAM_OPERATOR_ROLE = 5 | "0xaa630204f2780b6f080cc77cc0e9c0a5c21e92eb0c6771e709255dd27d6de132"; 6 | 7 | export const ROUND_ADMIN_ROLE = 8 | "0x0000000000000000000000000000000000000000000000000000000000000000"; 9 | 10 | export const ROUND_OPERATOR_ROLE = 11 | "0xec61da14b5abbac5c5fda6f1d57642a264ebd5d0674f35852829746dfb8174a5"; 12 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/roundMetaPtrUpdated.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import type { Indexer } from "../../indexer.js"; 3 | import { Changeset } from "../../../database/index.js"; 4 | import { Round } from "../../../database/schema.js"; 5 | import { parseAddress } from "../../../address.js"; 6 | 7 | export default async function ({ 8 | event, 9 | context: { ipfsGet, chainId }, 10 | }: EventHandlerArgs< 11 | Indexer, 12 | "AlloV1/RoundImplementation/V2", 13 | "RoundMetaPtrUpdated" 14 | >): Promise { 15 | const id = parseAddress(event.address); 16 | 17 | const metaPtr = event.params.newMetaPtr.pointer; 18 | const metadata = await ipfsGet(metaPtr); 19 | 20 | return [ 21 | { 22 | type: "UpdateRound", 23 | roundId: id, 24 | chainId, 25 | round: { 26 | roundMetadata: metadata ?? null, 27 | roundMetadataCid: event.params.newMetaPtr.pointer, 28 | }, 29 | }, 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /src/indexer/allo/v1/timeUpdated.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import type { Indexer } from "../../indexer.js"; 3 | 4 | import { parseAddress } from "../../../address.js"; 5 | import { Changeset } from "../../../database/index.js"; 6 | 7 | export async function updateApplicationsStartTime({ 8 | event, 9 | context: { chainId, db }, 10 | }: EventHandlerArgs< 11 | Indexer, 12 | "AlloV1/RoundImplementation/V2", 13 | "ApplicationsStartTimeUpdated" 14 | >): Promise { 15 | const id = parseAddress(event.address); 16 | const round = await db.getRoundById(chainId, id); 17 | const updatedTime = new Date(Number(event.params.newTime) * 1000); 18 | 19 | if (round === null) { 20 | throw new Error(`Round ${id} not found`); 21 | } 22 | 23 | return [ 24 | { 25 | type: "UpdateRound", 26 | roundId: round.id, 27 | chainId: round.chainId, 28 | round: { 29 | updatedAtBlock: event.blockNumber, 30 | applicationsStartTime: updatedTime, 31 | }, 32 | }, 33 | ]; 34 | } 35 | 36 | export async function updateApplicationsEndTime({ 37 | event, 38 | context: { chainId, db }, 39 | }: EventHandlerArgs< 40 | Indexer, 41 | "AlloV1/RoundImplementation/V2", 42 | "ApplicationsEndTimeUpdated" 43 | >): Promise { 44 | const id = parseAddress(event.address); 45 | const round = await db.getRoundById(chainId, id); 46 | const updatedTime = new Date(Number(event.params.newTime) * 1000); 47 | if (round === null) { 48 | throw new Error(`Round ${id} not found`); 49 | } 50 | 51 | return [ 52 | { 53 | type: "UpdateRound", 54 | roundId: round.id, 55 | chainId: round.chainId, 56 | round: { 57 | updatedAtBlock: event.blockNumber, 58 | applicationsEndTime: updatedTime, 59 | }, 60 | }, 61 | ]; 62 | } 63 | 64 | export async function updateDonationsStartTime({ 65 | event, 66 | context: { chainId, db }, 67 | }: EventHandlerArgs< 68 | Indexer, 69 | "AlloV1/RoundImplementation/V2", 70 | "RoundStartTimeUpdated" 71 | >): Promise { 72 | const id = parseAddress(event.address); 73 | const round = await db.getRoundById(chainId, id); 74 | const updatedTime = new Date(Number(event.params.newTime) * 1000); 75 | if (round === null) { 76 | throw new Error(`Round ${id} not found`); 77 | } 78 | 79 | return [ 80 | { 81 | type: "UpdateRound", 82 | roundId: round.id, 83 | chainId: round.chainId, 84 | round: { 85 | updatedAtBlock: event.blockNumber, 86 | donationsStartTime: updatedTime, 87 | }, 88 | }, 89 | ]; 90 | } 91 | 92 | export async function updateDonationsEndTime({ 93 | event, 94 | context: { chainId, db }, 95 | }: EventHandlerArgs< 96 | Indexer, 97 | "AlloV1/RoundImplementation/V2", 98 | "RoundEndTimeUpdated" 99 | >): Promise { 100 | const id = parseAddress(event.address); 101 | const round = await db.getRoundById(chainId, id); 102 | const updatedTime = new Date(Number(event.params.newTime) * 1000); 103 | if (round === null) { 104 | throw new Error(`Round ${id} not found`); 105 | } 106 | 107 | return [ 108 | { 109 | type: "UpdateRound", 110 | roundId: round.id, 111 | chainId: round.chainId, 112 | round: { 113 | updatedAtBlock: event.blockNumber, 114 | donationsEndTime: updatedTime, 115 | }, 116 | }, 117 | ]; 118 | } 119 | -------------------------------------------------------------------------------- /src/indexer/allo/v2/parsePoolMetadata.ts: -------------------------------------------------------------------------------- 1 | type ipfsGetFn = (cid: string) => Promise; 2 | 3 | type Metadata = { 4 | round: unknown; 5 | application: unknown; 6 | }; 7 | 8 | export async function fetchPoolMetadata(ipfsGet: ipfsGetFn, cid: string) { 9 | let roundMetadata: Metadata["round"] | null; 10 | let applicationMetadata: Metadata["application"] | null; 11 | const metadata = await ipfsGet(cid); 12 | 13 | if (metadata !== undefined && metadata.round !== undefined) { 14 | roundMetadata = metadata.round; 15 | } 16 | 17 | if (metadata !== undefined && metadata.application !== undefined) { 18 | applicationMetadata = metadata.application; 19 | } 20 | 21 | return { roundMetadata, applicationMetadata }; 22 | } 23 | -------------------------------------------------------------------------------- /src/indexer/allo/v2/poolMetadata.ts: -------------------------------------------------------------------------------- 1 | type IpfsGetFn = (cid: string) => Promise; 2 | 3 | type Metadata = { 4 | round: unknown; 5 | application: unknown; 6 | }; 7 | 8 | export async function fetchPoolMetadata(ipfsGet: IpfsGetFn, cid: string) { 9 | let roundMetadata: Metadata["round"] | null; 10 | let applicationMetadata: Metadata["application"] | null; 11 | const metadata = await ipfsGet(cid); 12 | 13 | if (metadata !== undefined && metadata.round !== undefined) { 14 | roundMetadata = metadata.round; 15 | } 16 | 17 | if (metadata !== undefined && metadata.application !== undefined) { 18 | applicationMetadata = metadata.application; 19 | } 20 | 21 | return { roundMetadata, applicationMetadata }; 22 | } 23 | -------------------------------------------------------------------------------- /src/indexer/allo/v2/roleGranted.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import type { Indexer } from "../../indexer.js"; 3 | import { Changeset } from "../../../database/index.js"; 4 | import { parseAddress } from "../../../address.js"; 5 | import { Round } from "../../../database/schema.js"; 6 | 7 | const ALLO_OWNER_ROLE = 8 | "0x815b5a78dc333d344c7df9da23c04dbd432015cc701876ddb9ffe850e6882747"; 9 | 10 | export default async function handleEvent( 11 | args: EventHandlerArgs< 12 | Indexer, 13 | "AlloV2/Registry/V1" | "AlloV2/Allo/V1", 14 | "RoleGranted" 15 | > 16 | ): Promise { 17 | const { 18 | chainId, 19 | event, 20 | context: { db }, 21 | } = args; 22 | 23 | switch (args.event.contractName) { 24 | case "AlloV2/Registry/V1": { 25 | const role = event.params.role.toLocaleLowerCase(); 26 | if (role === ALLO_OWNER_ROLE) { 27 | return []; 28 | } 29 | 30 | const account = parseAddress(event.params.account); 31 | const project = await db.getProjectById(chainId, role); 32 | // The member role for an Allo V2 profile, is the profileId itself. 33 | // If a project exists with that id, we create the member role 34 | // If it doesn't exists we create a pending project role. This can happens 35 | // when a new project is created, since in Allo V2 the RoleGranted event for a member is 36 | // emitted before the ProfileCreated event. 37 | if (project !== null) { 38 | return [ 39 | { 40 | type: "InsertProjectRole", 41 | projectRole: { 42 | chainId, 43 | projectId: project.id, 44 | address: account, 45 | role: "member", 46 | createdAtBlock: event.blockNumber, 47 | }, 48 | }, 49 | ]; 50 | } 51 | 52 | return [ 53 | { 54 | type: "InsertPendingProjectRole", 55 | pendingProjectRole: { 56 | chainId, 57 | role: role, 58 | address: account, 59 | createdAtBlock: event.blockNumber, 60 | }, 61 | }, 62 | ]; 63 | } 64 | 65 | case "AlloV2/Allo/V1": { 66 | const role = event.params.role.toLocaleLowerCase(); 67 | const account = parseAddress(event.params.account); 68 | 69 | let round: Round | null = null; 70 | 71 | // search for a round where the admin role is the role granted 72 | round = await db.getRoundByRole(chainId, "admin", role); 73 | if (round !== null) { 74 | return [ 75 | { 76 | type: "InsertRoundRole", 77 | roundRole: { 78 | chainId, 79 | roundId: round.id, 80 | role: "admin", 81 | address: account, 82 | createdAtBlock: event.blockNumber, 83 | }, 84 | }, 85 | ]; 86 | } 87 | 88 | // search for a round where the manager role is the role granted 89 | round = await db.getRoundByRole(chainId, "manager", role); 90 | if (round !== null) { 91 | return [ 92 | { 93 | type: "InsertRoundRole", 94 | roundRole: { 95 | chainId, 96 | roundId: round.id, 97 | role: "manager", 98 | address: account, 99 | createdAtBlock: event.blockNumber, 100 | }, 101 | }, 102 | ]; 103 | } 104 | 105 | return [ 106 | { 107 | type: "InsertPendingRoundRole", 108 | pendingRoundRole: { 109 | chainId, 110 | role: role, 111 | address: account, 112 | createdAtBlock: event.blockNumber, 113 | }, 114 | }, 115 | ]; 116 | } 117 | 118 | default: { 119 | return []; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/indexer/allo/v2/roleRevoked.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import type { Indexer } from "../../indexer.js"; 3 | import { Changeset } from "../../../database/index.js"; 4 | import { parseAddress } from "../../../address.js"; 5 | import { Round } from "../../../database/schema.js"; 6 | 7 | export default async function handleEvent( 8 | args: EventHandlerArgs< 9 | Indexer, 10 | "AlloV2/Registry/V1" | "AlloV2/Allo/V1", 11 | "RoleRevoked" 12 | > 13 | ): Promise { 14 | const { 15 | chainId, 16 | event, 17 | context: { db }, 18 | } = args; 19 | 20 | switch (args.event.contractName) { 21 | case "AlloV2/Registry/V1": { 22 | const account = parseAddress(event.params.account); 23 | const role = event.params.role.toLocaleLowerCase(); 24 | const project = await db.getProjectById(chainId, role); 25 | 26 | // The role value for a member is the profileId in Allo V1 27 | // which is the project id in this database. 28 | // If we don't find a project with that id we can't remove the role. 29 | if (project === null) { 30 | return []; 31 | } 32 | 33 | return [ 34 | { 35 | type: "DeleteAllProjectRolesByRoleAndAddress", 36 | projectRole: { 37 | chainId, 38 | projectId: project.id, 39 | address: account, 40 | role: "member", 41 | }, 42 | }, 43 | ]; 44 | } 45 | 46 | case "AlloV2/Allo/V1": { 47 | const role = event.params.role.toLocaleLowerCase(); 48 | const account = parseAddress(event.params.account); 49 | let round: Round | null = null; 50 | 51 | // search for a round where the admin role is the role granted 52 | round = await db.getRoundByRole(chainId, "admin", role); 53 | if (round !== null) { 54 | return [ 55 | { 56 | type: "DeleteAllRoundRolesByRoleAndAddress", 57 | roundRole: { 58 | chainId, 59 | roundId: round.id, 60 | address: account, 61 | role: "admin", 62 | }, 63 | }, 64 | ]; 65 | } 66 | 67 | // search for a round where the manager role is the role granted 68 | round = await db.getRoundByRole(chainId, "manager", role); 69 | if (round !== null) { 70 | return [ 71 | { 72 | type: "DeleteAllRoundRolesByRoleAndAddress", 73 | roundRole: { 74 | chainId, 75 | roundId: round.id, 76 | address: account, 77 | role: "manager", 78 | }, 79 | }, 80 | ]; 81 | } 82 | 83 | return []; 84 | } 85 | 86 | default: { 87 | return []; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/indexer/allo/v2/strategy.ts: -------------------------------------------------------------------------------- 1 | type Strategy = { 2 | id: string; 3 | name: string | null; 4 | groups: string[]; 5 | }; 6 | 7 | export function extractStrategyFromId(_id: string): Strategy | null { 8 | const id = _id.toLowerCase(); 9 | /* eslint-disable no-fallthrough */ 10 | switch (id) { 11 | // SQFSuperfluidv1 12 | case "0xf8a14294e80ff012e54157ec9d1b2827421f1e7f6bde38c06730b1c031b3f935": 13 | return { 14 | id: id, 15 | name: "allov2.SQFSuperFluidStrategy", 16 | groups: ["allov2.SQFSuperFluidStrategy"], 17 | }; 18 | 19 | // MicroGrantsv1 20 | case "0x697f0592ebd05466d2d24454477e11d69c475d7a7c4134f15ddc1ea9811bb16f": 21 | return { 22 | id: id, 23 | name: "allov2.MicroGrantsStrategy", 24 | groups: ["allov2.MicroGrantsStrategy", "allov2.MicroGrantsCommon"], 25 | }; 26 | 27 | // MicroGrantsGovv1 28 | case "0x741ac1e2f387d83f219f6b5349d35ec34902cf94019d117335e0045d2e0ed912": 29 | return { 30 | id: id, 31 | name: "allov2.MicroGrantsGovStrategy", 32 | groups: ["allov2.MicroGrantsGovStrategy", "allov2.MicroGrantsCommon"], 33 | }; 34 | 35 | // MicroGrantsHatsv1 36 | case "0x5aa24dcfcd55a1e059a172e987b3456736b4856c71e57aaf52e9a965897318dd": 37 | return { 38 | id: id, 39 | name: "allov2.MicroGrantsHatsStrategy", 40 | groups: ["allov2.MicroGrantsHatsStrategy", "allov2.MicroGrantsCommon"], 41 | }; 42 | 43 | // RFPSimpleStrategyv1.0 44 | case "0x0d459e12d9e91d2b2a8fa12be8c7eb2b4f1c35e74573990c34b436613bc2350f": 45 | return { 46 | id: id, 47 | name: "allov2.RFPSimpleStrategy", 48 | groups: ["allov2.RFPSimpleStrategy"], 49 | }; 50 | 51 | // RFPCommitteeStrategyv1.0 52 | case "0x7d143166a83c6a8a303ae32a6ccd287e48d79818f5d15d89e185391199909803": 53 | return { 54 | id: id, 55 | name: "allov2.RFPCommitteeStrategy", 56 | groups: ["allov2.RFPCommitteeStrategy"], 57 | }; 58 | 59 | // QVSimpleStrategyv1.0 60 | case "0x22d006e191d6dc5ff1a25bb0733f47f64a9c34860b6703df88dea7cb3987b4c3": 61 | return { 62 | id: id, 63 | name: "allov2.QVSimpleStrategy", 64 | groups: ["allov2.QVSimpleStrategy"], 65 | }; 66 | 67 | // DonationVotingMerkleDistributionDirectTransferStrategyv1.0 68 | case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": 69 | // DonationVotingMerkleDistributionDirectTransferStrategyv1.1 70 | case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce": 71 | // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 72 | case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b": 73 | // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 74 | case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": 75 | return { 76 | id: id, 77 | name: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy", 78 | groups: [ 79 | "allov2.DonationVotingMerkleDistributionDirectTransferStrategy", 80 | ], 81 | }; 82 | 83 | // DonationVotingMerkleDistributionVaultStrategyv1.0 84 | case "0x7e75375f0a7cd9f7ea159c8b065976e4f764f9dcef1edf692f31dd1842f70c87": 85 | // DonationVotingMerkleDistributionVaultStrategyv1.1 86 | case "0x093072375737c0e8872fef36808849aeba7f865e182d495f2b98308115c9ef13": 87 | return { 88 | id: id, 89 | name: "allov2.DonationVotingMerkleDistributionVaultStrategy", 90 | groups: ["allov2.DonationVotingMerkleDistributionVaultStrategy"], 91 | }; 92 | 93 | // DirectGrantsSimpleStrategyv1.1 94 | case "0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5": 95 | // DirectGrantsSimpleStrategyv2.1 96 | case "0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93": 97 | return { 98 | id: id, 99 | name: "allov2.DirectGrantsSimpleStrategy", 100 | groups: ["allov2.DirectGrantsSimpleStrategy"], 101 | }; 102 | 103 | // DirectGrantsLiteStrategyv1.0 104 | case "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27": 105 | return { 106 | id: id, 107 | name: "allov2.DirectGrantsLiteStrategy", 108 | groups: ["allov2.DirectGrantsLiteStrategy"], 109 | }; 110 | 111 | // EasyRPGFStrategy1.0 112 | case "0x662f5a0d3ea7e9b6ed1b351a9d96ac636a3c3ed727390aeff4ec931ae760d5ae": 113 | return { 114 | id: id, 115 | name: "allov2.EasyRPGFStrategy", 116 | groups: ["allov2.EasyRPGFStrategy"], 117 | }; 118 | 119 | // DirectAllocationStrategyv1.1 120 | case "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3": 121 | return { 122 | id: id, 123 | name: "allov2.DirectAllocationStrategy", 124 | groups: ["allov2.DirectAllocationStrategy"], 125 | }; 126 | 127 | // EasyRetroFundingStrategyv1.0 128 | case "0x060ffd6c79f918819a622248c6823443412aedea610cc19c89d28dadcdef7fba": 129 | return { 130 | id: id, 131 | name: "allov2.EasyRetroFundingStrategy", 132 | groups: ["allov2.EasyRetroFundingStrategy"], 133 | }; 134 | } 135 | 136 | return null; 137 | } 138 | -------------------------------------------------------------------------------- /src/indexer/applicationMetadata.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ApplicationMetadataSchema = z 4 | .object({ 5 | application: z.object({ 6 | round: z.string(), 7 | recipient: z.string(), 8 | }), 9 | }) 10 | .transform((data) => ({ type: "application" as const, ...data })); 11 | 12 | export type ApplicationMetadata = z.infer; 13 | -------------------------------------------------------------------------------- /src/indexer/gitcoin-attestation-network/handleEvent.ts: -------------------------------------------------------------------------------- 1 | import { EventHandlerArgs } from "chainsauce"; 2 | import { Hex, decodeAbiParameters } from "viem"; 3 | import { parseAddress } from "../../address.js"; 4 | import { Changeset } from "../../database/index.js"; 5 | import type { Indexer } from "../indexer.js"; 6 | import { 7 | AttestationMetadata, 8 | AttestationTxnData, 9 | GitcoinAttestedData, 10 | } from "../types.js"; 11 | import { getDateFromTimestamp } from "../../utils/index.js"; 12 | import { AttestationTable } from "../../database/schema.js"; 13 | 14 | function decodeAttestedData(encodedData: Hex) { 15 | const decodedData = decodeAbiParameters( 16 | [ 17 | { name: "projectsContributed", type: "uint64" }, 18 | { name: "roundsCountributed", type: "uint64" }, 19 | { name: "chainIdsContributed", type: "uint64" }, 20 | { name: "totalUSDAmount", type: "uint128" }, 21 | { name: "timestamp", type: "uint64" }, 22 | { name: "metadataCid", type: "string" }, 23 | ], 24 | encodedData 25 | ); 26 | 27 | const results: GitcoinAttestedData = { 28 | projectsContributed: decodedData[0], 29 | roundsCountributed: decodedData[1], 30 | chainIdsContributed: decodedData[2], 31 | totalUSDAmount: decodedData[3], 32 | timestamp: decodedData[4], 33 | metadataCid: decodedData[5], 34 | }; 35 | 36 | return results; 37 | } 38 | 39 | export async function handleEvent( 40 | args: EventHandlerArgs 41 | ): Promise { 42 | const { 43 | chainId, 44 | event, 45 | context: { ipfsGet, logger }, 46 | } = args; 47 | 48 | switch (event.name) { 49 | case "OnAttested": { 50 | const attestationId = event.params.uid; 51 | const recipient = parseAddress(event.params.recipient); 52 | const fee = event.params.fee; 53 | const refUID = event.params.refUID; 54 | 55 | const decodedAttestationData = decodeAttestedData(event.params.data); 56 | 57 | let data: AttestationMetadata[] = []; 58 | try { 59 | data = (await ipfsGet(decodedAttestationData.metadataCid)) ?? []; 60 | } catch (e) { 61 | logger.warn({ 62 | msg: `OnAttested: Failed to fetch metadata for attestation ${attestationId}`, 63 | event, 64 | decodedAttestationData, 65 | }); 66 | return []; 67 | } 68 | 69 | const transactionsData: AttestationTxnData[] = []; 70 | for (let i = 0; i < data.length; i++) { 71 | const metadata = data[i]; 72 | 73 | transactionsData.push({ 74 | chainId: metadata.chainId, 75 | txnHash: metadata.txnHash, 76 | }); 77 | } 78 | 79 | const attestationData: AttestationTable = { 80 | uid: attestationId, 81 | chainId: chainId, 82 | recipient: recipient, 83 | fee: fee, 84 | refUID: refUID, 85 | projectsContributed: decodedAttestationData.projectsContributed, 86 | roundsContributed: decodedAttestationData.roundsCountributed, 87 | chainIdsContributed: decodedAttestationData.chainIdsContributed, 88 | totalUSDAmount: decodedAttestationData.totalUSDAmount, 89 | timestamp: getDateFromTimestamp(decodedAttestationData.timestamp), 90 | metadataCid: decodedAttestationData.metadataCid, 91 | metadata: JSON.stringify(data), 92 | }; 93 | 94 | const changes: Changeset[] = [ 95 | { 96 | type: "InsertAttestation", 97 | attestation: { 98 | attestationData, 99 | transactionsData, 100 | }, 101 | }, 102 | ]; 103 | 104 | return changes; 105 | } 106 | } 107 | 108 | return []; 109 | } 110 | -------------------------------------------------------------------------------- /src/indexer/indexer.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "pino"; 2 | import { PriceProvider } from "../prices/provider.js"; 3 | import { Indexer as ChainsauceIndexer } from "chainsauce"; 4 | import { Database } from "../database/index.js"; 5 | import { PublicClient } from "viem"; 6 | 7 | import abis from "./abis/index.js"; 8 | 9 | export interface EventHandlerContext { 10 | chainId: number; 11 | db: Database; 12 | rpcClient: PublicClient; 13 | ipfsGet: (cid: string) => Promise; 14 | priceProvider: PriceProvider; 15 | blockTimestampInMs: (chainId: number, blockNumber: bigint) => Promise; 16 | logger: Logger; 17 | } 18 | 19 | export type Indexer = ChainsauceIndexer; 20 | -------------------------------------------------------------------------------- /src/indexer/projectMetadata.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ProjectMetadataSchema = z.union([ 4 | z 5 | .object({ 6 | title: z.string(), 7 | description: z.string(), 8 | }) 9 | .passthrough() 10 | .transform((data) => ({ type: "project" as const, ...data })), 11 | z 12 | .object({ 13 | canonical: z.object({ 14 | registryAddress: z.string(), 15 | chainId: z.coerce.number(), 16 | }), 17 | }) 18 | .transform((data) => ({ type: "project" as const, ...data })), 19 | z.object({ 20 | type: z.literal("program"), 21 | name: z.string(), 22 | }), 23 | z 24 | .object({ 25 | name: z.string(), 26 | }) 27 | .transform((data) => ({ type: "program" as const, ...data })), 28 | ]); 29 | 30 | export type ProjectMetadata = z.infer; 31 | -------------------------------------------------------------------------------- /src/indexer/types.ts: -------------------------------------------------------------------------------- 1 | export type DVMDApplicationData = { 2 | anchorAddress: string; 3 | recipientAddress: string; 4 | metadata: { 5 | protocol: number; 6 | pointer: string; 7 | }; 8 | }; 9 | 10 | export type DVMDExtendedApplicationData = DVMDApplicationData & { 11 | recipientsCounter: string; 12 | }; 13 | 14 | export type DGApplicationData = { 15 | recipientAddress: string; 16 | anchorAddress: string; 17 | grantAmount: bigint; 18 | metadata: { 19 | protocol: number; 20 | pointer: string; 21 | }; 22 | }; 23 | 24 | export type DVMDTimeStampUpdatedData = { 25 | registrationStartTime: bigint; 26 | registrationEndTime: bigint; 27 | allocationStartTime: bigint; 28 | allocationEndTime: bigint; 29 | }; 30 | 31 | export type EasyRetroFundingTimeStampUpdatedData = { 32 | registrationStartTime: bigint; 33 | registrationEndTime: bigint; 34 | poolStartTime: bigint; 35 | poolEndTime: bigint; 36 | }; 37 | 38 | export type DGTimeStampUpdatedData = { 39 | registrationStartTime: bigint; 40 | registrationEndTime: bigint; 41 | }; 42 | 43 | export type GitcoinAttestedData = { 44 | projectsContributed: bigint; 45 | roundsCountributed: bigint; 46 | chainIdsContributed: bigint; 47 | totalUSDAmount: bigint; 48 | timestamp: bigint; 49 | metadataCid: string; 50 | }; 51 | 52 | export type AttestationTxnData = { 53 | chainId: number; 54 | txnHash: string; 55 | impactImage?: string; 56 | }; 57 | 58 | export type AttestationProjectData = { 59 | id: string; 60 | title: string; 61 | anchor: string; 62 | applicationId: string; 63 | applicationCId: string; 64 | payoutAddress: string; 65 | roundId: number; 66 | strategy: string; 67 | amountInUSD: bigint; 68 | amount: bigint; 69 | token: string; 70 | }; 71 | 72 | export type AttestationMetadata = AttestationTxnData & { 73 | projects: AttestationProjectData[]; 74 | }; 75 | -------------------------------------------------------------------------------- /src/prices/coinGecko.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { ChainId, FetchInterface } from "../types.js"; 3 | import { Address } from "../types.js"; 4 | import { parseAddress } from "../address.js"; 5 | 6 | import retry from "async-retry"; 7 | 8 | const platforms: { [key: number]: string } = { 9 | 1: "ethereum", 10 | 250: "fantom", 11 | 10: "optimistic-ethereum", 12 | 42161: "arbitrum-one", 13 | 43114: "avalanche", 14 | 713715: "sei-network", 15 | 1329: "sei-network", 16 | 42: "lukso", 17 | 42220: "celo", 18 | 1088: "metis", 19 | 100: "xdai", 20 | 295: "hedera-hashgraph", 21 | }; 22 | 23 | const nativeTokens: { [key: number]: string } = { 24 | 1: "ethereum", 25 | 250: "fantom", 26 | 10: "ethereum", 27 | 42161: "ethereum", 28 | 43114: "avalanche-2", 29 | 713715: "sei-network", 30 | 1329: "sei-network", 31 | 42: "lukso-token", 32 | 42220: "celo", 33 | 1088: "metis-token", 34 | 100: "xdai", 35 | 295: "hedera-hashgraph", 36 | }; 37 | 38 | type TimestampInMs = number; 39 | type Price = number; 40 | 41 | export async function fetchPricesForRange({ 42 | chainId, 43 | tokenAddress, 44 | startTimestampInMs, 45 | endTimestampInMs, 46 | coingeckoApiKey, 47 | coingeckoApiUrl, 48 | fetch, 49 | }: { 50 | chainId: ChainId; 51 | tokenAddress: Address; 52 | startTimestampInMs: number; 53 | endTimestampInMs: number; 54 | coingeckoApiKey: string | null; 55 | coingeckoApiUrl: string; 56 | fetch: FetchInterface; 57 | }): Promise<[TimestampInMs, Price][]> { 58 | const platform = platforms[chainId]; 59 | const nativeToken = nativeTokens[chainId]; 60 | 61 | if (!(chainId in platforms)) { 62 | throw new Error(`Prices for chain ID ${chainId} are not supported.`); 63 | } 64 | 65 | const isNativeToken = 66 | tokenAddress === parseAddress(ethers.constants.AddressZero); 67 | 68 | const startTimestampInSecs = Math.floor(startTimestampInMs / 1000); 69 | const endTimestampInSecs = Math.floor(endTimestampInMs / 1000); 70 | 71 | const path = isNativeToken 72 | ? `/coins/${nativeToken}/market_chart/range?vs_currency=usd&from=${startTimestampInSecs}&to=${endTimestampInSecs}` 73 | : `/coins/${platform}/contract/${tokenAddress.toLowerCase()}/market_chart/range?vs_currency=usd&from=${startTimestampInSecs}&to=${endTimestampInSecs}`; 74 | 75 | const headers: HeadersInit = 76 | coingeckoApiKey === null 77 | ? {} 78 | : { 79 | "x-cg-pro-api-key": coingeckoApiKey, 80 | }; 81 | 82 | const responseBody = await retry( 83 | async () => { 84 | const res = await fetch(`${coingeckoApiUrl}${path}`, { 85 | headers, 86 | }); 87 | 88 | const body = (await res.json()) as 89 | | { prices: Array<[TimestampInMs, Price]> } 90 | | { error: string } 91 | | { status: { error_code: number; error_message: string } }; 92 | 93 | if (res.status === 429) { 94 | throw new Error( 95 | `CoinGecko API rate limit exceeded, are you using an API key?` 96 | ); 97 | } 98 | 99 | return body; 100 | }, 101 | { 102 | retries: 4, 103 | minTimeout: 4000, 104 | } 105 | ); 106 | 107 | if ("error" in responseBody) { 108 | throw new Error( 109 | `Error from CoinGecko API: ${JSON.stringify(responseBody)}` 110 | ); 111 | } 112 | 113 | if ("status" in responseBody) { 114 | throw new Error( 115 | `Error from CoinGecko API: ${JSON.stringify( 116 | responseBody.status.error_message 117 | )}` 118 | ); 119 | } 120 | 121 | return responseBody.prices; 122 | } 123 | -------------------------------------------------------------------------------- /src/prices/common.ts: -------------------------------------------------------------------------------- 1 | export class UnknownTokenError extends Error { 2 | public constructor( 3 | public address: string, 4 | public chainId: number, 5 | message?: string 6 | ) { 7 | super(message ?? `Token ${address} not configured for chain ${chainId}`); 8 | this.name = new.target.name; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/resourceMonitor.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, expect, test, describe } from "vitest"; 2 | 3 | import { createResourceMonitor } from "./resourceMonitor.js"; 4 | import { pino } from "pino"; 5 | 6 | describe("ResourceMonitor", () => { 7 | test("reporting at interval", async () => { 8 | vi.useFakeTimers(); 9 | const logger = pino(); 10 | 11 | const logInfoMock = vi.spyOn(logger, "info").mockImplementation(() => {}); 12 | 13 | const monitor = createResourceMonitor({ 14 | logger, 15 | diskstats: { 16 | check: () => 17 | Promise.resolve({ 18 | total: 100, 19 | used: 50, 20 | inodes: { 21 | total: 100, 22 | used: 60, 23 | }, 24 | }), 25 | }, 26 | directories: ["/"], 27 | pollingIntervalMs: 1000, 28 | }); 29 | 30 | monitor.start(); 31 | 32 | await vi.runOnlyPendingTimersAsync(); 33 | 34 | expect(logInfoMock).toHaveBeenCalledTimes(2); 35 | 36 | expect(logInfoMock).toHaveBeenCalledWith({ 37 | type: "disk", 38 | directory: "/", 39 | total: 100, 40 | used: 50, 41 | }); 42 | 43 | expect(logInfoMock).toHaveBeenCalledWith({ 44 | type: "inode", 45 | directory: "/", 46 | total: 100, 47 | used: 60, 48 | }); 49 | 50 | // loggin should continue after the first interval 51 | logInfoMock.mockClear(); 52 | await vi.runOnlyPendingTimersAsync(); 53 | 54 | expect(logInfoMock).toHaveBeenCalledTimes(2); 55 | 56 | expect(logInfoMock).toHaveBeenCalledWith({ 57 | type: "disk", 58 | directory: "/", 59 | total: 100, 60 | used: 50, 61 | }); 62 | 63 | expect(logInfoMock).toHaveBeenCalledWith({ 64 | type: "inode", 65 | directory: "/", 66 | total: 100, 67 | used: 60, 68 | }); 69 | 70 | monitor.stop(); 71 | }); 72 | 73 | test("stop monitor", async () => { 74 | vi.useFakeTimers(); 75 | const logger = pino(); 76 | 77 | const logInfoMock = vi.spyOn(logger, "info").mockImplementation(() => {}); 78 | 79 | const monitor = createResourceMonitor({ 80 | logger, 81 | diskstats: { 82 | check: () => 83 | Promise.resolve({ 84 | total: 100, 85 | used: 50, 86 | inodes: { 87 | total: 100, 88 | used: 60, 89 | }, 90 | }), 91 | }, 92 | directories: ["/"], 93 | pollingIntervalMs: 1000, 94 | }); 95 | 96 | monitor.start(); 97 | 98 | // first log 99 | await vi.runOnlyPendingTimersAsync(); 100 | 101 | expect(logInfoMock).toHaveBeenCalledTimes(2); 102 | 103 | expect(logInfoMock).toHaveBeenCalledWith({ 104 | type: "disk", 105 | directory: "/", 106 | total: 100, 107 | used: 50, 108 | }); 109 | 110 | expect(logInfoMock).toHaveBeenCalledWith({ 111 | type: "inode", 112 | directory: "/", 113 | total: 100, 114 | used: 60, 115 | }); 116 | 117 | // no reporting should happen after the monitor is stopped 118 | logInfoMock.mockClear(); 119 | monitor.stop(); 120 | 121 | await vi.runOnlyPendingTimersAsync(); 122 | 123 | expect(logInfoMock).toHaveBeenCalledTimes(0); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/resourceMonitor.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "pino"; 2 | 3 | interface ResourceMonitor { 4 | start: () => void; 5 | stop: () => void; 6 | } 7 | 8 | interface ResourceMonitorConfig { 9 | diskstats: typeof import("diskstats"); 10 | logger: Logger; 11 | directories: string[]; 12 | pollingIntervalMs: number; 13 | } 14 | 15 | export function createResourceMonitor({ 16 | logger, 17 | diskstats, 18 | directories, 19 | pollingIntervalMs, 20 | }: ResourceMonitorConfig): ResourceMonitor { 21 | let pollingTimer: NodeJS.Timeout | null = null; 22 | 23 | const pollResources = async () => { 24 | for (const directory of directories) { 25 | const stats = await diskstats.check(directory); 26 | 27 | const totalDiskSpace = stats.total; 28 | const totalDiskUsed = stats.used; 29 | 30 | logger.info({ 31 | type: "disk", 32 | directory, 33 | total: totalDiskSpace, 34 | used: totalDiskUsed, 35 | }); 36 | 37 | const totalInodes = stats.inodes.total; 38 | const totalInodesUsed = stats.inodes.used; 39 | 40 | logger.info({ 41 | type: "inode", 42 | directory, 43 | total: totalInodes, 44 | used: totalInodesUsed, 45 | }); 46 | } 47 | 48 | if (pollingTimer) { 49 | pollingTimer = setTimeout(pollResources, pollingIntervalMs); 50 | } 51 | }; 52 | 53 | const start = () => { 54 | if (pollingTimer !== null) { 55 | throw new Error("ResourceMonitor already started"); 56 | } 57 | 58 | pollingTimer = setTimeout(pollResources, 0); 59 | }; 60 | 61 | const stop = () => { 62 | if (pollingTimer === null) { 63 | throw new Error("ResourceMonitor not started"); 64 | } 65 | 66 | clearTimeout(pollingTimer); 67 | pollingTimer = null; 68 | }; 69 | 70 | return { 71 | start, 72 | stop, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/test/calculator/proportionalMatch.test.fixtures.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DeprecatedVote, 3 | DeprecatedRound, 4 | DeprecatedApplication, 5 | } from "../../deprecatedJsonDatabase.js"; 6 | import { test as baseTest } from "vitest"; 7 | import { Chain } from "../../config.js"; 8 | import type { PassportScore } from "../../passport/index.js"; 9 | import type { PassportProvider } from "../../passport/index.js"; 10 | import { AddressToPassportScoreMap } from "../../passport/index.js"; 11 | import { isPresent } from "ts-is-present"; 12 | 13 | export class FakePassportProvider implements PassportProvider { 14 | scores: { 15 | [address: string]: PassportScore | undefined; 16 | }; 17 | 18 | constructor(scores: PassportScore[]) { 19 | this.scores = {}; 20 | for (const s of scores) { 21 | this.scores[s.address] = s; 22 | } 23 | } 24 | 25 | start(_opts?: { watch: boolean } | undefined) { 26 | return Promise.resolve(undefined); 27 | } 28 | 29 | stop(): void {} 30 | 31 | getScoreByAddress(address: string) { 32 | return Promise.resolve(this.scores[address]); 33 | } 34 | 35 | getScoresByAddresses( 36 | addresses: string[] 37 | ): Promise { 38 | return Promise.resolve( 39 | new Map( 40 | addresses 41 | .map((address) => this.scores[address]) 42 | .filter(isPresent) 43 | .map((score) => [score.address, score]) 44 | ) 45 | ); 46 | } 47 | } 48 | 49 | const SAMPLE_VOTES_AND_SCORES = [ 50 | { id: 1, amount: 1000n, rawScore: "0.0" }, 51 | { id: 2, amount: 1000n, rawScore: "10.0" }, 52 | { id: 3, amount: 1000n, rawScore: "15.0" }, 53 | { id: 4, amount: 1000n, rawScore: "20.0" }, 54 | { id: 5, amount: 1000n, rawScore: "25.0" }, 55 | { id: 6, amount: 1000n, rawScore: "30.0" }, 56 | ]; 57 | 58 | const round: DeprecatedRound = { 59 | id: "0x1234", 60 | amountUSD: 0, 61 | votes: 0, 62 | token: "0x1234", 63 | matchAmount: "0x0", 64 | matchAmountUSD: 0, 65 | uniqueContributors: 0, 66 | applicationMetaPtr: "", 67 | applicationMetadata: null, 68 | metaPtr: "", 69 | metadata: null, 70 | applicationsStartTime: "", 71 | applicationsEndTime: "", 72 | roundStartTime: "", 73 | roundEndTime: "", 74 | createdAtBlock: 0, 75 | updatedAtBlock: 0, 76 | }; 77 | 78 | const applications: DeprecatedApplication[] = [ 79 | { 80 | id: "application-id-1", 81 | projectId: "project-id-1", 82 | anchorAddress: "0x1234", 83 | roundId: "0x1234", 84 | status: "APPROVED", 85 | amountUSD: 0, 86 | votes: 0, 87 | uniqueContributors: 0, 88 | metadata: { 89 | application: { 90 | project: { 91 | title: "", 92 | website: "", 93 | projectTwitter: "", 94 | projectGithub: "", 95 | userGithub: "", 96 | }, 97 | answers: [], 98 | recipient: "grant-address-1", 99 | }, 100 | }, 101 | createdAtBlock: 0, 102 | statusUpdatedAtBlock: 0, 103 | statusSnapshots: [ 104 | { 105 | status: "APPROVED", 106 | statusUpdatedAtBlock: 0, 107 | }, 108 | ], 109 | }, 110 | ]; 111 | 112 | const chain = { 113 | id: 250, 114 | tokens: [ 115 | { 116 | code: "GcV", 117 | address: "0x83791638da5EB2fAa432aff1c65fbA47c5D29510", 118 | voteAmountCap: BigInt(10e18), 119 | }, 120 | { 121 | code: "Dummy", 122 | address: "0x1234", 123 | }, 124 | ], 125 | } as unknown as Chain; 126 | 127 | function generateVoteAndScore(id: number, amount: bigint, rawScore: string) { 128 | const vote: DeprecatedVote = { 129 | id: `vote-${id}`, 130 | projectId: "project-id-1", 131 | applicationId: "application-id-1", 132 | roundId: "round-id-1", 133 | token: "0x83791638da5EB2fAa432aff1c65fbA47c5D29510", 134 | voter: `voter-${id}`, 135 | grantAddress: "grant-address-1", 136 | transaction: "0x1234", 137 | blockNumber: 0, 138 | amount: amount.toString(), 139 | amountUSD: Number(amount), 140 | amountRoundToken: amount.toString(), 141 | }; 142 | 143 | const score = { 144 | address: `voter-${id}`, 145 | score: "xyz", 146 | status: "XYZ", 147 | last_score_timestamp: "2023-05-08T10:17:52.872812+00:00", 148 | evidence: { 149 | type: "ThresholdScoreCheck", 150 | // success is not used anymore 151 | success: false, 152 | // rawScore is the only attribute used 153 | rawScore, 154 | // threshold is not used 155 | threshold: "15.00000", 156 | }, 157 | error: null, 158 | }; 159 | 160 | return { vote, score }; 161 | } 162 | 163 | function generateData() { 164 | const votes: DeprecatedVote[] = []; 165 | const scores: PassportScore[] = []; 166 | 167 | const passportScoresByAddress: AddressToPassportScoreMap = new Map(); 168 | 169 | SAMPLE_VOTES_AND_SCORES.forEach(({ id, amount, rawScore }) => { 170 | const { vote, score } = generateVoteAndScore(id, amount, rawScore); 171 | votes.push(vote); 172 | scores.push(score); 173 | passportScoresByAddress.set(score.address, score); 174 | }); 175 | 176 | return { votes, scores, passportScoresByAddress }; 177 | } 178 | 179 | export const test = baseTest.extend<{ 180 | round: DeprecatedRound; 181 | applications: DeprecatedApplication[]; 182 | chain: Chain; 183 | data: { 184 | votes: DeprecatedVote[]; 185 | scores: PassportScore[]; 186 | passportScoresByAddress: AddressToPassportScoreMap; 187 | }; 188 | }>({ 189 | round, 190 | applications, 191 | chain, 192 | data: async ({ task: _task }, use) => { 193 | await use(generateData()); 194 | }, 195 | }); 196 | -------------------------------------------------------------------------------- /src/test/calculator/proportionalMatch.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, beforeAll } from "vitest"; 2 | import { getVotesWithCoefficients } from "../../calculator/votes.js"; 3 | import { test } from "./proportionalMatch.test.fixtures.js"; 4 | 5 | describe("getVotesWithCoefficients", () => { 6 | beforeAll(() => {}); 7 | 8 | describe("should update the amount proportionally based on the passport score", () => { 9 | test("returns votes with amounts updated proportionally based on passport score", ({ 10 | chain, 11 | round, 12 | applications, 13 | data, 14 | }) => { 15 | const res = getVotesWithCoefficients({ 16 | chain, 17 | round, 18 | applications, 19 | votes: data.votes, 20 | passportScoreByAddress: data.passportScoresByAddress, 21 | enablePassport: true, 22 | }); 23 | 24 | const expectedData = [ 25 | { id: 1, amount: 1000n, rawScore: "0.0", coefficient: 0 }, 26 | { id: 2, amount: 1000n, rawScore: "10.0", coefficient: 0 }, 27 | { id: 3, amount: 1000n, rawScore: "15.0", coefficient: 0.5 }, 28 | { id: 4, amount: 1000n, rawScore: "20.0", coefficient: 0.75 }, 29 | { id: 5, amount: 1000n, rawScore: "25.0", coefficient: 1 }, 30 | { id: 6, amount: 1000n, rawScore: "30.0", coefficient: 1 }, 31 | ]; 32 | 33 | expect(res.length).toEqual(6); 34 | 35 | for (let i = 0; i < expectedData.length; i++) { 36 | const vote = res[i]; 37 | const expected = expectedData[i]; 38 | 39 | expect(vote.id).toEqual(`vote-${expected.id}`); 40 | expect(vote.amount).toEqual(expected.amount.toString()); 41 | expect(vote.passportScore!.evidence!.rawScore).toEqual( 42 | expected.rawScore 43 | ); 44 | expect(vote.coefficient).toEqual(expected.coefficient); 45 | } 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/test/calculator/votes.test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DeprecatedVote, 3 | DeprecatedRound, 4 | DeprecatedApplication, 5 | } from "../../deprecatedJsonDatabase.js"; 6 | import { describe, test, expect } from "vitest"; 7 | import { getVotesWithCoefficients } from "../../calculator/votes.js"; 8 | import { Chain } from "../../config.js"; 9 | 10 | const round: DeprecatedRound = { 11 | id: "0x1234", 12 | amountUSD: 0, 13 | votes: 0, 14 | token: "0x1234", 15 | matchAmount: "0x0", 16 | matchAmountUSD: 0, 17 | uniqueContributors: 0, 18 | applicationMetaPtr: "", 19 | applicationMetadata: null, 20 | metaPtr: "", 21 | metadata: null, 22 | applicationsStartTime: "", 23 | applicationsEndTime: "", 24 | roundStartTime: "", 25 | roundEndTime: "", 26 | createdAtBlock: 0, 27 | updatedAtBlock: 0, 28 | }; 29 | 30 | const applications: DeprecatedApplication[] = [ 31 | { 32 | id: "application-id-1", 33 | projectId: "project-id-1", 34 | anchorAddress: "0x1234", 35 | roundId: "0x1234", 36 | status: "APPROVED", 37 | amountUSD: 0, 38 | votes: 0, 39 | uniqueContributors: 0, 40 | metadata: { 41 | application: { 42 | project: { 43 | title: "", 44 | website: "", 45 | projectTwitter: "", 46 | projectGithub: "", 47 | userGithub: "", 48 | }, 49 | answers: [], 50 | recipient: "grant-address-1", 51 | }, 52 | }, 53 | createdAtBlock: 0, 54 | statusUpdatedAtBlock: 0, 55 | statusSnapshots: [ 56 | { 57 | status: "APPROVED", 58 | statusUpdatedAtBlock: 0, 59 | }, 60 | ], 61 | }, 62 | ]; 63 | 64 | const votes: DeprecatedVote[] = [ 65 | // expected to be capped to 10 tokens 66 | { 67 | id: "vote-1", 68 | projectId: "project-id-1", 69 | applicationId: "application-id-1", 70 | roundId: "round-id-1", 71 | token: "0x83791638da5EB2fAa432aff1c65fbA47c5D29510", 72 | voter: "voter-1", 73 | grantAddress: "grant-address-1", 74 | // higher than the cap (10e18) 75 | amount: BigInt(20e18).toString(), 76 | amountUSD: 20, 77 | amountRoundToken: BigInt(50e18).toString(), 78 | transaction: "0x1234", 79 | blockNumber: 0, 80 | }, 81 | 82 | // not expected to be capped to 10 tokens 83 | // because token is not in the token settings 84 | { 85 | id: "vote-2", 86 | projectId: "project-id-1", 87 | applicationId: "application-id-1", 88 | roundId: "round-id-1", 89 | token: "0x1234", 90 | voter: "voter-1", 91 | grantAddress: "grant-address-1", 92 | // higher than the cap (10e18) 93 | amount: BigInt(20e18).toString(), 94 | amountUSD: 20, 95 | amountRoundToken: BigInt(50e18).toString(), 96 | transaction: "0x1234", 97 | blockNumber: 0, 98 | }, 99 | ]; 100 | 101 | const MOCK_CHAIN = { 102 | id: 250, 103 | tokens: [ 104 | { 105 | code: "GcV", 106 | address: "0x83791638da5EB2fAa432aff1c65fbA47c5D29510", 107 | voteAmountCap: BigInt(10e18), 108 | }, 109 | { 110 | code: "Dummy", 111 | address: "0x1234", 112 | }, 113 | ], 114 | } as unknown as Chain; 115 | 116 | describe("getVotesWithCoefficients", () => { 117 | describe("should take voteAmountCap into conisderation", () => { 118 | test("returns capped vote if capping is defined for token", () => { 119 | const testVoteIndex = 0; 120 | 121 | const res = getVotesWithCoefficients({ 122 | chain: MOCK_CHAIN, 123 | round, 124 | applications, 125 | votes, 126 | passportScoreByAddress: new Map(), 127 | }); 128 | 129 | expect(res[testVoteIndex]).toEqual({ 130 | ...votes[testVoteIndex], 131 | coefficient: 1, 132 | amountRoundToken: BigInt(25e18).toString(), 133 | }); 134 | }); 135 | 136 | test("doesn't cap votes if capping isn't defined for token", () => { 137 | const testVoteIndex = 1; 138 | 139 | const res = getVotesWithCoefficients({ 140 | chain: MOCK_CHAIN, 141 | round, 142 | applications, 143 | votes, 144 | passportScoreByAddress: new Map(), 145 | }); 146 | 147 | expect(res[testVoteIndex]).toEqual({ 148 | ...votes[testVoteIndex], 149 | coefficient: 1, 150 | }); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /src/test/fixtures/applications.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "application-id-1", 4 | "projectId": "project-id-1", 5 | "status": "APPROVED", 6 | "metadata": { 7 | "application": { 8 | "recipient": "grant-address-1" 9 | } 10 | } 11 | }, 12 | { 13 | "id": "application-id-2", 14 | "projectId": "project-id-2", 15 | "status": "APPROVED", 16 | "metadata": { 17 | "application": { 18 | "recipient": "grant-address-2" 19 | } 20 | } 21 | }, 22 | { 23 | "id": "application-id-3", 24 | "projectId": "project-id-3", 25 | "status": "APPROVED", 26 | "metadata": { 27 | "application": { 28 | "recipient": "grant-address-3" 29 | } 30 | } 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /src/test/fixtures/overrides-with-floating-coefficient.csv: -------------------------------------------------------------------------------- 1 | id,coefficient 2 | vote-1,0.5 3 | vote-2,0.5 4 | vote-3,0.5 5 | vote-4,0.5 6 | -------------------------------------------------------------------------------- /src/test/fixtures/overrides-with-invalid-coefficient.csv: -------------------------------------------------------------------------------- 1 | id,coefficient 2 | vote-1,0 3 | vote-2,what 4 | vote-3,0 5 | vote-4,0 6 | -------------------------------------------------------------------------------- /src/test/fixtures/overrides-without-coefficient.csv: -------------------------------------------------------------------------------- 1 | id,badCoefficientColumn 2 | vote-1,0 3 | vote-2,0 4 | vote-3,0 5 | vote-4,0 6 | -------------------------------------------------------------------------------- /src/test/fixtures/overrides-without-transaction-id.csv: -------------------------------------------------------------------------------- 1 | badContributionIdColumn,coefficient 2 | vote-1,0 3 | vote-2,0 4 | vote-3,0 5 | vote-4,0 6 | -------------------------------------------------------------------------------- /src/test/fixtures/overrides.csv: -------------------------------------------------------------------------------- 1 | id,coefficient 2 | vote-1,0 3 | vote-2,0 4 | vote-3,0 5 | vote-4,0 6 | -------------------------------------------------------------------------------- /src/test/fixtures/passport_scores.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": "voter-1", 4 | "score": "1.000000000", 5 | "status": "DONE", 6 | "last_score_timestamp": "2023-05-08T10:17:52.872812+00:00", 7 | "evidence": { 8 | "type": "ThresholdScoreCheck", 9 | "success": true, 10 | "rawScore": "27.21", 11 | "threshold": "15.00000" 12 | }, 13 | "error": null 14 | }, 15 | { 16 | "address": "voter-2", 17 | "score": "1.000000000", 18 | "status": "DONE", 19 | "last_score_timestamp": "2023-05-08T10:17:52.872812+00:00", 20 | "evidence": { 21 | "type": "ThresholdScoreCheck", 22 | "success": false, 23 | "rawScore": "10.03", 24 | "threshold": "15.00000" 25 | }, 26 | "error": null 27 | }, 28 | { 29 | "address": "voter-3", 30 | "score": "1.000000000", 31 | "status": "DONE", 32 | "last_score_timestamp": "2023-05-08T10:17:52.872812+00:00", 33 | "evidence": { 34 | "type": "ThresholdScoreCheck", 35 | "success": true, 36 | "rawScore": "30.03", 37 | "threshold": "15.00000" 38 | }, 39 | "error": null 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /src/test/fixtures/rounds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "0x0000000000000000000000000000000000000001", 4 | "token": "0x0000000000000000000000000000000000000000", 5 | "matchAmount": "10000", 6 | "metadata": {} 7 | }, 8 | { 9 | "id": "0x0000000000000000000000000000000000000002", 10 | "token": "0x0000000000000000000000000000000000000000", 11 | "matchAmount": "10000", 12 | "metadata": { 13 | "quadraticFundingConfig": { 14 | "sybilDefense": "passport" 15 | } 16 | } 17 | }, 18 | { 19 | "id": "0x0000000000000000000000000000000000000003", 20 | "token": "0x0000000000000000000000000000000000000000", 21 | "matchAmount": "10000", 22 | "metadata": { 23 | "quadraticFundingConfig": { 24 | "matchingCap": true, 25 | "matchingCapAmount": 10 26 | } 27 | } 28 | }, 29 | { 30 | "id": "0x0000000000000000000000000000000000000004", 31 | "token": "0x0000000000000000000000000000000000000000", 32 | "matchAmount": "10000", 33 | "metadata": { 34 | "quadraticFundingConfig": { 35 | "minDonationThresholdAmount": "3" 36 | } 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /src/test/fixtures/votes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "vote-1", 4 | "projectId": "project-id-1", 5 | "applicationId": "application-id-1", 6 | "roundId": "round-id-1", 7 | "token": "0x0000000000000000000000000000000000000000", 8 | "voter": "voter-1", 9 | "grantAddress": "grant-address-1", 10 | "amount": "0", 11 | "amountUSD": 1, 12 | "amountRoundToken": "100" 13 | }, 14 | { 15 | "id": "vote-2", 16 | "projectId": "project-id-1", 17 | "applicationId": "application-id-1", 18 | "roundId": "round-id-1", 19 | "token": "0x0000000000000000000000000000000000000000", 20 | "voter": "voter-2", 21 | "grantAddress": "grant-address-1", 22 | "amount": "0", 23 | "amountUSD": 4, 24 | "amountRoundToken": "400" 25 | }, 26 | { 27 | "id": "vote-3", 28 | "projectId": "project-id-1", 29 | "applicationId": "application-id-1", 30 | "roundId": "round-id-1", 31 | "token": "0x0000000000000000000000000000000000000000", 32 | "voter": "voter-3", 33 | "grantAddress": "grant-address-1", 34 | "amount": "0", 35 | "amountUSD": 1, 36 | "amountRoundToken": "100" 37 | }, 38 | { 39 | "id": "vote-4", 40 | "projectId": "project-id-1", 41 | "applicationId": "application-id-1", 42 | "roundId": "round-id-1", 43 | "token": "0x0000000000000000000000000000000000000000", 44 | "voter": "voter-4", 45 | "grantAddress": "grant-address-1", 46 | "amount": "0", 47 | "amountUSD": 9, 48 | "amountRoundToken": "900" 49 | }, 50 | 51 | { 52 | "id": "vote-5", 53 | "projectId": "project-id-2", 54 | "applicationId": "application-id-2", 55 | "roundId": "round-id-1", 56 | "token": "0x0000000000000000000000000000000000000000", 57 | "voter": "voter-5", 58 | "grantAddress": "grant-address-2", 59 | "amount": "0", 60 | "amountUSD": 1, 61 | "amountRoundToken": "100" 62 | }, 63 | { 64 | "id": "vote-6", 65 | "projectId": "project-id-2", 66 | "applicationId": "application-id-2", 67 | "roundId": "round-id-1", 68 | "token": "0x0000000000000000000000000000000000000000", 69 | "voter": "voter-6", 70 | "grantAddress": "grant-address-2", 71 | "amount": "0", 72 | "amountUSD": 1, 73 | "amountRoundToken": "100" 74 | }, 75 | { 76 | "id": "vote-7", 77 | "projectId": "project-id-2", 78 | "applicationId": "application-id-2", 79 | "roundId": "round-id-1", 80 | "token": "0x0000000000000000000000000000000000000000", 81 | "voter": "voter-7", 82 | "grantAddress": "grant-address-2", 83 | "amount": "0", 84 | "amountUSD": 1, 85 | "amountRoundToken": "100" 86 | }, 87 | { 88 | "id": "vote-8", 89 | "projectId": "project-id-2", 90 | "applicationId": "application-id-2", 91 | "roundId": "round-id-1", 92 | "token": "0x0000000000000000000000000000000000000000", 93 | "voter": "voter-8", 94 | "grantAddress": "grant-address-2", 95 | "amount": "0", 96 | "amountUSD": 1, 97 | "amountRoundToken": "100" 98 | }, 99 | { 100 | "id": "vote-9", 101 | "projectId": "project-id-2", 102 | "applicationId": "application-id-2", 103 | "roundId": "round-id-1", 104 | "token": "0x0000000000000000000000000000000000000000", 105 | "voter": "voter-9", 106 | "grantAddress": "grant-address-2", 107 | "amount": "0", 108 | "amountUSD": 1, 109 | "amountRoundToken": "100" 110 | }, 111 | { 112 | "id": "vote-10", 113 | "projectId": "project-id-2", 114 | "applicationId": "application-id-2", 115 | "roundId": "round-id-1", 116 | "token": "0x0000000000000000000000000000000000000000", 117 | "voter": "voter-10", 118 | "grantAddress": "grant-address-2", 119 | "amount": "0", 120 | "amountUSD": 1, 121 | "amountRoundToken": "100" 122 | }, 123 | { 124 | "id": "vote-11", 125 | "projectId": "project-id-2", 126 | "applicationId": "application-id-2", 127 | "roundId": "round-id-1", 128 | "token": "0x0000000000000000000000000000000000000000", 129 | "voter": "voter-11", 130 | "grantAddress": "grant-address-2", 131 | "amount": "0", 132 | "amountUSD": 4, 133 | "amountRoundToken": "400" 134 | }, 135 | 136 | { 137 | "id": "vote-12", 138 | "projectId": "project-id-3", 139 | "applicationId": "application-id-3", 140 | "roundId": "round-id-1", 141 | "token": "0x0000000000000000000000000000000000000000", 142 | "voter": "voter-12", 143 | "grantAddress": "grant-address-3", 144 | "amount": "0", 145 | "amountUSD": 1, 146 | "amountRoundToken": "100" 147 | }, 148 | { 149 | "id": "vote-13", 150 | "projectId": "project-id-3", 151 | "applicationId": "application-id-3", 152 | "roundId": "round-id-1", 153 | "token": "0x0000000000000000000000000000000000000000", 154 | "voter": "voter-13", 155 | "grantAddress": "grant-address-3", 156 | "amount": "0", 157 | "amountUSD": 9, 158 | "amountRoundToken": "900" 159 | }, 160 | { 161 | "id": "vote-14", 162 | "projectId": "project-id-3", 163 | "applicationId": "application-id-3", 164 | "roundId": "round-id-1", 165 | "token": "0x0000000000000000000000000000000000000000", 166 | "voter": "voter-14", 167 | "grantAddress": "grant-address-3", 168 | "amount": "0", 169 | "amountUSD": 1, 170 | "amountRoundToken": "100" 171 | }, 172 | { 173 | "id": "vote-15", 174 | "projectId": "project-id-3", 175 | "applicationId": "application-id-3", 176 | "roundId": "round-id-1", 177 | "token": "0x0000000000000000000000000000000000000000", 178 | "voter": "voter-15", 179 | "grantAddress": "grant-address-3", 180 | "amount": "0", 181 | "amountUSD": 9, 182 | "amountRoundToken": "900" 183 | }, 184 | { 185 | "id": "vote-16", 186 | "projectId": "project-id-3", 187 | "applicationId": "application-id-3", 188 | "roundId": "round-id-1", 189 | "token": "0x0000000000000000000000000000000000000000", 190 | "voter": "voter-16", 191 | "grantAddress": "grant-address-3", 192 | "amount": "0", 193 | "amountUSD": 1, 194 | "amountRoundToken": "100" 195 | }, 196 | { 197 | "id": "vote-17", 198 | "projectId": "project-id-3", 199 | "applicationId": "application-id-3", 200 | "roundId": "round-id-1", 201 | "token": "0x0000000000000000000000000000000000000000", 202 | "voter": "voter-17", 203 | "grantAddress": "grant-address-3", 204 | "amount": "0", 205 | "amountUSD": 9, 206 | "amountRoundToken": "900" 207 | }, 208 | { 209 | "id": "vote-18", 210 | "projectId": "project-id-3", 211 | "applicationId": "application-id-3", 212 | "roundId": "round-id-1", 213 | "token": "0x0000000000000000000000000000000000000000", 214 | "voter": "voter-18", 215 | "grantAddress": "grant-address-3", 216 | "amount": "0", 217 | "amountUSD": 4, 218 | "amountRoundToken": "400" 219 | } 220 | ] 221 | -------------------------------------------------------------------------------- /src/test/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { Address } from "../types.js"; 4 | import { DataProvider } from "../calculator/dataProvider/index.js"; 5 | import { FileNotFoundError } from "../calculator/errors.js"; 6 | import { 7 | AddressToPassportScoreMap, 8 | PassportProvider, 9 | PassportScore, 10 | } from "../passport/index.js"; 11 | import { isPresent } from "ts-is-present"; 12 | import { PriceProvider, PriceWithDecimals } from "../prices/provider.js"; 13 | 14 | type Fixtures = { [path: string]: string | undefined | unknown[] }; 15 | 16 | export const loadFixture = async ( 17 | name: string, 18 | extension = "json" 19 | ): Promise => { 20 | const p = path.resolve(__dirname, "./fixtures", `${name}.${extension}`); 21 | const data = await fs.readFile(p, "utf8"); 22 | return data; 23 | }; 24 | 25 | export class TestPassportProvider implements PassportProvider { 26 | _fixture: PassportScore[] | null = null; 27 | 28 | async start() {} 29 | 30 | async stop() {} 31 | 32 | async getScoreByAddress(address: string): Promise { 33 | if (this._fixture === null) { 34 | this._fixture = JSON.parse( 35 | await loadFixture("passport_scores") 36 | ) as PassportScore[]; 37 | } 38 | return this._fixture.find((score) => score.address === address); 39 | } 40 | 41 | async getScoresByAddresses( 42 | addresses: string[] 43 | ): Promise { 44 | if (this._fixture === null) { 45 | this._fixture = JSON.parse( 46 | await loadFixture("passport_scores") 47 | ) as PassportScore[]; 48 | } 49 | const fixture = this._fixture; 50 | return new Map( 51 | addresses 52 | .map((address) => fixture.find((score) => score.address === address)) 53 | .filter(isPresent) 54 | .map((score) => [score.address, score]) 55 | ); 56 | } 57 | } 58 | 59 | export class TestDataProvider implements DataProvider { 60 | fixtures: Fixtures; 61 | 62 | constructor(fixtures: Fixtures) { 63 | this.fixtures = fixtures; 64 | } 65 | 66 | async loadFile(description: string, path: string): Promise> { 67 | const fixture = this.fixtures[path]; 68 | if (fixture === undefined) { 69 | throw new FileNotFoundError(description); 70 | } 71 | 72 | if (typeof fixture !== "string") { 73 | return fixture as Array; 74 | } 75 | 76 | return JSON.parse(await loadFixture(fixture)) as Array; 77 | } 78 | } 79 | 80 | export class TestPriceProvider implements PriceProvider { 81 | async getUSDConversionRate( 82 | chainId: number, 83 | tokenAddress: Address, 84 | blockNumber: bigint | "latest" 85 | ): Promise { 86 | return Promise.resolve({ 87 | id: 0, 88 | tokenDecimals: 18, 89 | chainId: chainId, 90 | priceInUsd: 1, 91 | tokenAddress: tokenAddress, 92 | blockNumber: blockNumber === "latest" ? 0n : blockNumber, 93 | timestamp: new Date(), 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/tokenMath.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { convertTokenToFiat, convertFiatToToken } from "./tokenMath.js"; 3 | 4 | describe("tokenMath", () => { 5 | test("token amount to currency", () => { 6 | expect( 7 | convertTokenToFiat({ 8 | tokenAmount: 3400000000000000000n, 9 | tokenDecimals: 18, 10 | tokenPrice: 1, 11 | tokenPriceDecimals: 8, 12 | }) 13 | ).toBe(3.4); 14 | 15 | expect( 16 | convertTokenToFiat({ 17 | tokenAmount: 50000000000n, 18 | tokenDecimals: 18, 19 | tokenPrice: 1, 20 | tokenPriceDecimals: 8, 21 | }) 22 | ).toBe(0.00000005); 23 | 24 | expect( 25 | convertTokenToFiat({ 26 | tokenAmount: 3400000000000000000n, 27 | tokenDecimals: 18, 28 | tokenPrice: 0.5, 29 | tokenPriceDecimals: 8, 30 | }) 31 | ).toBe(1.7); 32 | 33 | expect( 34 | convertTokenToFiat({ 35 | tokenAmount: 3400000000000000000n, 36 | tokenDecimals: 18, 37 | tokenPrice: 2, 38 | tokenPriceDecimals: 8, 39 | }) 40 | ).toBe(6.8); 41 | }); 42 | 43 | test("currency to token amount", () => { 44 | expect( 45 | convertFiatToToken({ 46 | fiatAmount: 3.4, 47 | tokenPrice: 1, 48 | tokenPriceDecimals: 8, 49 | tokenDecimals: 18, 50 | }) 51 | ).toBe(3400000000000000000n); 52 | 53 | expect( 54 | convertFiatToToken({ 55 | fiatAmount: 3.4, 56 | tokenPrice: 2, 57 | tokenPriceDecimals: 8, 58 | tokenDecimals: 18, 59 | }) 60 | ).toBe(1700000000000000000n); 61 | 62 | expect( 63 | convertFiatToToken({ 64 | fiatAmount: 3.4, 65 | tokenPrice: 0.5, 66 | tokenPriceDecimals: 8, 67 | tokenDecimals: 18, 68 | }) 69 | ).toBe(6800000000000000000n); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/tokenMath.ts: -------------------------------------------------------------------------------- 1 | /** Converts a token amount (fixed point as a bigint) to a fiat currency amount (number) 2 | * @param args.tokenAmount The token amount 3 | * @param args.tokenDecimals The token decimals 4 | * @param args.tokenPrice The price of the token to fiat 5 | * @param args.tokenPriceDecimals The decimal places to use for the price, defaults to 8, any decimals beyond this will be truncated 6 | * @returns The currency amount 7 | * */ 8 | export function convertTokenToFiat(args: { 9 | tokenAmount: bigint; 10 | tokenDecimals: number; 11 | tokenPrice: number; 12 | tokenPriceDecimals: number; 13 | }): number { 14 | const priceDecimalFactor = Math.pow(10, args.tokenPriceDecimals); 15 | const fixedPointPrice = BigInt( 16 | Math.trunc(args.tokenPrice * priceDecimalFactor) 17 | ); 18 | 19 | const tokenDecimalFactor = 10n ** BigInt(args.tokenDecimals); 20 | 21 | return ( 22 | Number((args.tokenAmount * fixedPointPrice) / tokenDecimalFactor) / 23 | priceDecimalFactor 24 | ); 25 | } 26 | 27 | /** Converts a fiat amount (number) to a token amount (fixed point as a bigint) 28 | * @param args.currency The currency amount 29 | * @param args.tokenPrice The price of the token to fiat 30 | * @param args.tokenPriceDecimals The decimal places to use for the price, defaults to 8, any decimals beyond this will be truncated 31 | * @param args.tokenDecimals The decimals of the token we converting to 32 | * @returns The token amount 33 | * */ 34 | export function convertFiatToToken(args: { 35 | fiatAmount: number; 36 | tokenDecimals: number; 37 | tokenPrice: number; 38 | tokenPriceDecimals: number; 39 | }): bigint { 40 | if (args.fiatAmount === 0) { 41 | return 0n; 42 | } 43 | 44 | const priceDecimalFactor = Math.pow(10, args.tokenPriceDecimals); 45 | 46 | const fiatAmountBigInt = BigInt( 47 | Math.trunc((args.fiatAmount / args.tokenPrice) * priceDecimalFactor) 48 | ); 49 | 50 | const tokenDecimalFactor = 10n ** BigInt(args.tokenDecimals); 51 | 52 | return (fiatAmountBigInt * tokenDecimalFactor) / BigInt(priceDecimalFactor); 53 | } 54 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export { Address } from "./address.js"; 2 | 3 | export type Hex = `0x${string}`; 4 | export type ChainId = number; 5 | 6 | import type { FetchOptions } from "make-fetch-happen"; 7 | import type { Response } from "node-fetch"; 8 | 9 | export type FetchInterface = ( 10 | uri: string, 11 | opts?: FetchOptions 12 | ) => Promise; 13 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: why is eslint not recognizing type narrowing? 2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 3 | 4 | import { Logger } from "pino"; 5 | 6 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 7 | export function encodeJsonWithBigInts(value: unknown): string { 8 | return JSON.stringify(value, (_key, value) => { 9 | if (typeof value === "bigint") { 10 | return { type: "bigint", value: value.toString() }; 11 | } 12 | return value as unknown; 13 | }); 14 | } 15 | 16 | export function decodeJsonWithBigInts(encodedJson: string): T { 17 | return JSON.parse(encodedJson, (_key, value) => { 18 | if ( 19 | typeof value === "object" && 20 | value !== null && 21 | "type" in value && 22 | value.type === "bigint" && 23 | "value" in value && 24 | typeof value.value === "string" 25 | ) { 26 | return BigInt(value.value); 27 | } 28 | return value as unknown; 29 | }) as T; 30 | } 31 | 32 | export const UINT64_MAX = 18446744073709551615n; 33 | 34 | export const getDateFromTimestamp = (timestamp: bigint): Date | null => { 35 | return timestamp < UINT64_MAX ? new Date(Number(timestamp) * 1000) : null; 36 | }; 37 | 38 | export const getExternalIP = async (logger: Logger): Promise => { 39 | const urls = ["https://api.ipify.org?format=json", "http://ipinfo.io/json"]; 40 | for (const url of urls) { 41 | try { 42 | logger.debug(`Attempting to fetch IP address from: ${url}`); 43 | const response = await fetch(url); 44 | if (response.ok) { 45 | const { ip } = (await response.json()) as { ip: string }; 46 | logger.info(`Successfully fetched IP address: ${ip}`); 47 | return ip; 48 | } 49 | throw new Error(`Request failed with status: ${response.status}`); 50 | } catch (error) { 51 | if (error instanceof Error) { 52 | logger.error(`Failed to fetch from ${url}: ${error.message}`); 53 | } else { 54 | logger.error(`Failed to fetch from ${url}`); 55 | } 56 | } 57 | } 58 | throw new Error( 59 | "Unable to fetch external IP address from both primary and fallback URLs." 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "target": "es2022", 5 | "module": "NodeNext", 6 | "allowJs": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "nodenext", 9 | "allowSyntheticDefaultImports": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "outDir": "dist", 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "declaration": false, 17 | "sourceMap": true 18 | }, 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { configDefaults, defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | coverage: { 7 | exclude: [".var"], 8 | }, 9 | watchExclude: [...configDefaults.watchExclude, ".var", "test"], 10 | exclude: [...configDefaults.exclude, ".var"], 11 | }, 12 | }); 13 | --------------------------------------------------------------------------------