diff --git a/.dockerignore b/.dockerignore
index 20b25939ec..3b54c5b8c4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1 +1,2 @@
-**/dist/
\ No newline at end of file
+**/dist/
+**.env
diff --git a/.github/workflows/app-deploy-feature-branch.yml b/.github/workflows/app-deploy-feature-branch.yml
index 8d3debee5a..e7bc4fca09 100644
--- a/.github/workflows/app-deploy-feature-branch.yml
+++ b/.github/workflows/app-deploy-feature-branch.yml
@@ -70,7 +70,7 @@ jobs:
uses: ./.github/workflows/app-e2e.yml
secrets: inherit
permissions:
- contents: read
+ contents: write
with:
targetUrl: ${{ needs.build.outputs.dappUrl }}
testnet_network_value_for_e2e: "/?network=sepolia"
diff --git a/.github/workflows/app-deploy-preview.yml b/.github/workflows/app-deploy-preview.yml
index 36fa0db2c1..b2208e0662 100644
--- a/.github/workflows/app-deploy-preview.yml
+++ b/.github/workflows/app-deploy-preview.yml
@@ -67,11 +67,10 @@ jobs:
uses: ./.github/workflows/app-e2e.yml
secrets: inherit
permissions:
- contents: read
+ contents: write
with:
targetUrl: ${{ needs.deploy.outputs.dappUrl }}
testnet_network_value_for_e2e: "/?network=sepolia"
default_network_value_for_e2e: "/?network=mainnet"
publish_to_allure: true
environmentTags: "and not @featureEnv"
-
\ No newline at end of file
diff --git a/.github/workflows/app-e2e.yml b/.github/workflows/app-e2e.yml
index 6d629e4f72..0803e7976d 100644
--- a/.github/workflows/app-e2e.yml
+++ b/.github/workflows/app-e2e.yml
@@ -79,8 +79,6 @@ jobs:
uses: actions/cache@v3
env:
cache-name: cache-node-modules
- # Workaround for bug https://github.com/typicode/husky/issues/991
- HUSKY: 0
with:
path: node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
@@ -116,27 +114,21 @@ jobs:
E2ENETWORK='${{ inputs.default_network_value_for_e2e }}' npx cucumber-js --tags "${{ matrix.tags }} ${{ inputs.environmentTags }} and not @testnet"
fi
- - name: Reset tags quotes
+ - name: Save artifacts to Git
if: always()
- run: |
- echo "MATRIX_TAG_WITHOUT_QUOTES=$(echo ${{ matrix.tags }} | sed -e 's/@//g' )" >> $GITHUB_ENV
+ uses: actions/upload-artifact@v3
+ with:
+ name: allure-results
+ path: packages/app/allure-results
- - name: Create launch ID
+ - name: Upload test results to Allure reporter
if: always()
env:
- ALLURE_LAUNCH_NAME: "#${{ github.run_number }} ${{ env.MATRIX_TAG_WITHOUT_QUOTES }}"
- ALLURE_LAUNCH_TAGS: "${{ env.ALLURE_BASIC_TAGS }}, ${{ env.MATRIX_TAG_WITHOUT_QUOTES }}"
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
run: |
- echo "ALLURE_LAUNCH_ID=$(./allurectl launch create --launch-name '${{ env.ALLURE_LAUNCH_NAME }}' --no-header --format ID | tail -n1)" >> $GITHUB_ENV
-
- - name: Upload tests to the Allure proj
- if: always() && inputs.publish_to_allure == true
- env:
- ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
- run: |
- ./allurectl upload allure-results --launch-id ${{ env.ALLURE_LAUNCH_ID }}
- ./allurectl launch close ${{ env.ALLURE_LAUNCH_ID }}
+ ./allurectl upload allure-results
+ echo "*Public report link: https://raw.githack.com/matter-labs/block-explorer/gh-pages/${{ github.run_number }}/index.html"
+ echo "*Public report link will be available when the 'Allure Report' job will be succesfully executed."
- if: failure()
name: Save artifacts
@@ -146,18 +138,53 @@ jobs:
path: packages/app/tests/e2e/artifacts/*
publish:
- name: Publish Allure link to GIT
+ name: Allure Report
runs-on: ubuntu-latest
permissions:
- contents: read
+ contents: write
needs: e2e
if: always()
steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/download-artifact@v2
+ with:
+ name: allure-results
+ path: packages/app/allure-results
+
+ - name: Get Allure history
+ uses: actions/checkout@v3
+ if: always()
+ continue-on-error: true
+ with:
+ ref: gh-pages
+ path: gh-pages
+
+ - name: Allure Report action from marketplace
+ uses: simple-elf/allure-report-action@v1.7
+ if: always()
+ id: allure-report
+ with:
+ allure_results: packages/app/allure-results
+ gh_pages: gh-pages
+ allure_report: allure-report
+ allure_history: allure-history
+ keep_reports: 10
+
+ - name: Deploy report to Github Pages
+ if: always()
+ uses: peaceiris/actions-gh-pages@v2
+ env:
+ PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PUBLISH_BRANCH: gh-pages
+ PUBLISH_DIR: allure-history
+
- name: Prepare a link
run: |
echo "BASE64_SEARCH_REQUEST=$(echo '${{ env.ALLURE_SEARCH_REQUEST }}' | base64)" >> $GITHUB_ENV
- name: Publish Allure link to GIT Summary
run: |
- LINK="${{ vars.ALLURE_ENDPOINT }}project/${{ vars.ALLURE_PROJECT_ID }}/launches?search=${{ env.BASE64_SEARCH_REQUEST }}"
- echo "Allure [e2e tests]($LINK) :rocket: in git run #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
+ LINK1="${{ vars.ALLURE_ENDPOINT }}project/${{ vars.ALLURE_PROJECT_ID }}/launches?search=${{ env.BASE64_SEARCH_REQUEST }}"
+ LINK2="https://raw.githack.com/matter-labs/block-explorer/gh-pages/${{ github.run_number }}/index.html"
+ echo "Allure [Private]($LINK1) and [Public]($LINK2) links:rocket: in git run #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 74b9091810..8415c96558 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -111,6 +111,20 @@ jobs:
file: packages/worker/Dockerfile
no-cache: true
+ - name: Build and push Docker image for Data Fetcher
+ uses: docker/build-push-action@v4
+ with:
+ push: true
+ tags: |
+ "matterlabs/block-explorer-data-fetcher:latest"
+ "matterlabs/block-explorer-data-fetcher:v${{ needs.createReleaseVersion.outputs.releaseVersion }}"
+ "matterlabs/block-explorer-data-fetcher:${{ steps.setVersionForFlux.outputs.imageTag }}"
+ "us-docker.pkg.dev/matterlabs-infra/matterlabs-docker/block-explorer-data-fetcher:latest"
+ "us-docker.pkg.dev/matterlabs-infra/matterlabs-docker/block-explorer-data-fetcher:v${{ needs.createReleaseVersion.outputs.releaseVersion }}"
+ "us-docker.pkg.dev/matterlabs-infra/matterlabs-docker/block-explorer-data-fetcher:${{ steps.setVersionForFlux.outputs.imageTag }}"
+ file: packages/data-fetcher/Dockerfile
+ no-cache: true
+
- name: Build and push Docker image for App
uses: docker/build-push-action@v4
with:
diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml
index 539a8e0679..a837a4a49e 100644
--- a/.github/workflows/validate-pr.yml
+++ b/.github/workflows/validate-pr.yml
@@ -66,6 +66,7 @@ jobs:
packages/app/junit.xml
packages/api/junit.xml
packages/worker/junit.xml
+ packages/data-fetcher/junit.xml
check_run_annotations: all tests, skipped tests
report_individual_runs: "true"
check_name: Unit Test Results
diff --git a/.gitignore b/.gitignore
index 159e3cd3e1..dd8c346732 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@ tests/e2e/reports/
# Logs
logs
!/packages/worker/test/logs/
+!/packages/data-fetcher/test/logs/
*.log
npm-debug.log*
yarn-debug.log*
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..c359855985
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "editor.tabSize": 2
+}
diff --git a/README.md b/README.md
index 1af83554ed..5fa1535b62 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,9 @@
Online blockchain browser for viewing and analyzing zkSync Era blockchain.
## 📌 Overview
-This repository is a monorepo consisting of 3 packages:
-- [Worker](./packages/worker) - an indexer service for [zkSync Era](https://zksync.io) blockchain data. The purpose of the service is to read the data from the blockchain in real time, transform it and fill in it's database with the data in a way that makes it easy to be queried by the [API](./packages/api) service.
+This repository is a monorepo consisting of 4 packages:
+- [Worker](./packages/worker) - an indexer service for [zkSync Era](https://zksync.io) blockchain data. The purpose of the service is to read blockchain data in real time, transform it and fill in it's database with the data in a way that makes it easy to be queried by the [API](./packages/api) service.
+- [Data Fetcher](./packages/data-fetcher) - a service that exposes and implements an HTTP endpoint to retrieve aggregated data for a certain block / range of blocks from the blockchain. This endpoint is called by the [Worker](./packages/worker) service.
- [API](./packages/api) - a service providing Web API for retrieving structured [zkSync Era](https://zksync.io) blockchain data collected by [Worker](./packages/worker). It connects to the Worker's database to be able to query the collected data.
- [App](./packages/app) - a front-end app providing an easy-to-use interface for users to view and inspect transactions, blocks, contracts and more. It makes requests to the [API](./packages/api) to get the data and presents it in a way that's easy to read and understand.
@@ -22,10 +23,14 @@ flowchart
subgraph explorer[Block explorer]
Database[("Block explorer DB (PostgreSQL)")]
Worker(Worker service)
+ Data-Fetcher(Data Fetcher service)
API(API service)
App(App)
-
+
+ Worker-."Request aggregated data (HTTP)".->Data-Fetcher
+ Data-Fetcher-."Request data (HTTP)".->Blockchain
Worker-.Save processed data.->Database
+
API-.Query data.->Database
App-."Request data (HTTP)".->API
App-."Request data (HTTP)".->Blockchain
@@ -34,7 +39,7 @@ flowchart
Worker-."Request data (HTTP)".->Blockchain
```
-[Worker](./packages/worker) service is responsible for getting data from blockchain using [zkSync Era JSON-RPC API](https://era.zksync.io/docs/api/api.html), processing it and saving into the database. [API](./packages/api) service is connected to the same database where it gets the data from to handle API requests. It performs only read requests to the database. The front-end [App](./packages/app) makes HTTP calls to the Block Explorer [API](./packages/api) to get blockchain data and to the [zkSync Era JSON-RPC API](https://era.zksync.io/docs/api/api.html) for reading contracts, performing transactions etc.
+[Worker](./packages/worker) service retrieves aggregated data from the [Data Fetcher](./packages/data-fetcher) via HTTP and also directly from the blockchain using [zkSync Era JSON-RPC API](https://era.zksync.io/docs/api/api.html), processes it and saves into the database. [API](./packages/api) service is connected to the same database where it gets the data from to handle API requests. It performs only read requests to the database. The front-end [App](./packages/app) makes HTTP calls to the Block Explorer [API](./packages/api) to get blockchain data and to the [zkSync Era JSON-RPC API](https://era.zksync.io/docs/api/api.html) for reading contracts, performing transactions etc.
## 🚀 Features
@@ -58,12 +63,12 @@ npm install
## ⚙️ Setting up env variables
### Manually set up env variables
-Make sure you have set up all the necessary env variables. Follow [Setting up env variables for Worker](./packages/worker#setting-up-env-variables) and [Setting up env variables for API](./packages/api#setting-up-env-variables) for instructions. For the [App](./packages/app) package you might want to edit environment config, see [Environment configs](./packages/app#environment-configs).
+Make sure you have set up all the necessary env variables. Follow setting up env variables instructions for [Worker](./packages/worker#setting-up-env-variables), [Data Fetcher](./packages/data-fetcher#setting-up-env-variables) and [API](./packages/api#setting-up-env-variables). For the [App](./packages/app) package you might want to edit environment config, see [Environment configs](./packages/app#environment-configs).
### Build env variables based on your [zksync-era](https://github.com/matter-labs/zksync-era) local repo setup
Make sure you have [zksync-era](https://github.com/matter-labs/zksync-era) repo set up locally. You must have your environment variables files present in the [zksync-era](https://github.com/matter-labs/zksync-era) repo at `/etc/env/*.env` for the build envs script to work.
-The following script sets `.env` files for [Worker](./packages/worker) and [API](./packages/api) packages as well as environment configuration file for [App](./packages/app) package based on your local [zksync-era](https://github.com/matter-labs/zksync-era) repo setup.
+The following script sets `.env` files for [Worker](./packages/worker), [Data Fetcher](./packages/data-fetcher) and [API](./packages/api) packages as well as environment configuration file for [App](./packages/app) package based on your local [zksync-era](https://github.com/matter-labs/zksync-era) repo setup.
```bash
npm run hyperchain:configure
```
@@ -77,7 +82,7 @@ To create a database run the following command:
npm run db:create
```
-To run all the packages (`Worker`, `API` and front-end `App`) in `development` mode run the following command from the root directory.
+To run all the packages (`Worker`, `Data Fetcher`, `API` and front-end `App`) in `development` mode run the following command from the root directory.
```bash
npm run dev
```
@@ -102,7 +107,7 @@ To get block-explorer connected to your ZK Stack Hyperchain you need to set up a
## 🔍 Verify Block Explorer is up and running
-To verify front-end `App` is running open http://localhost:3010 in your browser. `API` should be available at http://localhost:3020. `Worker` - http://localhost:3001.
+To verify front-end `App` is running open http://localhost:3010 in your browser. `API` should be available at http://localhost:3020, `Worker` at http://localhost:3001 and `Data Fetcher` at http://localhost:3040.
## 🕵️♂️ Testing
Run unit tests for all packages:
diff --git a/docker-compose-cli.yaml b/docker-compose-cli.yaml
index 1343eb8fa2..a8f29e8478 100644
--- a/docker-compose-cli.yaml
+++ b/docker-compose-cli.yaml
@@ -1,10 +1,7 @@
-version: '3.2'
-
services:
app:
- build:
- context: .
- dockerfile: ./packages/app/Dockerfile
+ platform: linux/amd64
+ image: "matterlabs/block-explorer-app:${VERSION}"
ports:
- '3010:3010'
depends_on:
@@ -12,9 +9,8 @@ services:
restart: unless-stopped
worker:
- build:
- context: .
- dockerfile: ./packages/worker/Dockerfile
+ platform: linux/amd64
+ image: "matterlabs/block-explorer-worker:${VERSION}"
environment:
- PORT=3001
- LOG_LEVEL=verbose
@@ -23,14 +19,30 @@ services:
- DATABASE_USER=postgres
- DATABASE_PASSWORD=postgres
- DATABASE_NAME=block-explorer
- - BLOCKCHAIN_RPC_URL=http://host.docker.internal:3050
+ - BLOCKCHAIN_RPC_URL=http://host.docker.internal:${RPC_PORT}
+ - DATA_FETCHER_URL=http://data-fetcher:3040
- BATCHES_PROCESSING_POLLING_INTERVAL=1000
restart: unless-stopped
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+
+ data-fetcher:
+ platform: linux/amd64
+ image: "matterlabs/block-explorer-data-fetcher:${VERSION}"
+ environment:
+ - PORT=3040
+ - LOG_LEVEL=verbose
+ - NODE_ENV=development
+ - BLOCKCHAIN_RPC_URL=http://host.docker.internal:${RPC_PORT}
+ ports:
+ - '3040:3040'
+ restart: unless-stopped
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
api:
- build:
- context: .
- dockerfile: ./packages/api/Dockerfile
+ platform: linux/amd64
+ image: "matterlabs/block-explorer-api:${VERSION}"
environment:
- PORT=3020
- METRICS_PORT=3005
@@ -60,4 +72,4 @@ services:
- POSTGRES_DB=block-explorer
volumes:
- postgres:
+ postgres:
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 22834bc826..59eeb73f9a 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,17 +1,10 @@
-version: '3.8'
-
services:
app:
build:
context: .
dockerfile: ./packages/app/Dockerfile
- target: development-stage
- command: npm run --prefix packages/app dev -- --host
ports:
- '3010:3010'
- volumes:
- - ./packages/app:/usr/src/app/packages/app
- - /usr/src/app/packages/app/node_modules
depends_on:
- api
restart: unless-stopped
@@ -20,25 +13,33 @@ services:
build:
context: .
dockerfile: ./packages/worker/Dockerfile
- target: development-stage
- command: npm run --prefix packages/worker dev:debug
environment:
- PORT=3001
- LOG_LEVEL=verbose
- - NODE_ENV=development
- DATABASE_HOST=postgres
- DATABASE_USER=postgres
- DATABASE_PASSWORD=postgres
- DATABASE_NAME=block-explorer
- BLOCKCHAIN_RPC_URL=http://zksync:3050
+ - DATA_FETCHER_URL=http://data-fetcher:3040
- BATCHES_PROCESSING_POLLING_INTERVAL=1000
ports:
- '3001:3001'
- - '9229:9229'
- - '9230:9230'
- volumes:
- - ./packages/worker:/usr/src/app/packages/worker
- - /usr/src/app/packages/worker/node_modules
+ depends_on:
+ zksync:
+ condition: service_healthy
+ restart: unless-stopped
+
+ data-fetcher:
+ build:
+ context: .
+ dockerfile: ./packages/data-fetcher/Dockerfile
+ environment:
+ - PORT=3040
+ - LOG_LEVEL=verbose
+ - BLOCKCHAIN_RPC_URL=http://zksync:3050
+ ports:
+ - '3040:3040'
depends_on:
zksync:
condition: service_healthy
@@ -48,22 +49,14 @@ services:
build:
context: .
dockerfile: ./packages/api/Dockerfile
- target: development-stage
- command: npm run --prefix packages/api dev:debug
environment:
- PORT=3020
- METRICS_PORT=3005
- LOG_LEVEL=verbose
- - NODE_ENV=development
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/block-explorer
ports:
- '3020:3020'
- '3005:3005'
- - '9231:9229'
- - '9232:9230'
- volumes:
- - ./packages/api:/usr/src/app/packages/api
- - /usr/src/app/packages/api/node_modules
depends_on:
- worker
restart: unless-stopped
@@ -120,11 +113,11 @@ services:
test: "curl -H \"Content-Type: application/json\" -X POST --data '{\"jsonrpc\":\"2.0\",\"method\":\"web3_clientVersion\",\"params\":[],\"id\":67}' 127.0.0.1:3050 || exit 1"
interval: 5s
timeout: 5s
- retries: 120
+ retries: 300
restart: unless-stopped
volumes:
geth:
postgres:
zksync-config:
- zksync-data:
\ No newline at end of file
+ zksync-data:
diff --git a/package-lock.json b/package-lock.json
index fefc9cd23d..be64827c1b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25196,6 +25196,10 @@
"node": ">=8"
}
},
+ "node_modules/data-fetcher": {
+ "resolved": "packages/data-fetcher",
+ "link": true
+ },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -58093,6 +58097,205 @@
"typedarray-to-buffer": "^3.1.5"
}
},
+ "packages/data-fetcher": {
+ "version": "0.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "@nestjs/common": "^9.0.0",
+ "@nestjs/config": "^2.2.0",
+ "@nestjs/core": "^9.0.0",
+ "@nestjs/platform-express": "^9.0.0",
+ "@nestjs/terminus": "^9.1.2",
+ "@willsoto/nestjs-prometheus": "^4.7.0",
+ "ethers": "^5.7.1",
+ "nest-winston": "^1.7.0",
+ "prom-client": "^14.1.0",
+ "reflect-metadata": "^0.1.13",
+ "rimraf": "^3.0.2",
+ "rxjs": "^7.2.0",
+ "winston": "^3.8.2",
+ "zksync-web3": "0.15.4"
+ },
+ "devDependencies": {
+ "@nestjs/cli": "^9.0.0",
+ "@nestjs/schematics": "^9.0.0",
+ "@nestjs/testing": "^9.0.0",
+ "@types/express": "^4.17.13",
+ "@types/jest": "28.1.8",
+ "@types/supertest": "^2.0.11",
+ "@typescript-eslint/eslint-plugin": "^5.0.0",
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint-config-prettier": "^8.3.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "jest": "29.2.1",
+ "jest-junit": "^14.0.1",
+ "jest-mock-extended": "^3.0.1",
+ "lint-staged": "^13.0.3",
+ "source-map-support": "^0.5.20",
+ "supertest": "^6.1.3",
+ "ts-jest": "29.0.3",
+ "ts-loader": "^9.2.3",
+ "ts-node": "^10.0.0",
+ "tsconfig-paths": "4.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0"
+ }
+ },
+ "packages/data-fetcher/node_modules/@ethersproject/networks": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz",
+ "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.buymeacoffee.com/ricmoo"
+ }
+ ],
+ "dependencies": {
+ "@ethersproject/logger": "^5.7.0"
+ }
+ },
+ "packages/data-fetcher/node_modules/@ethersproject/providers": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz",
+ "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.buymeacoffee.com/ricmoo"
+ }
+ ],
+ "dependencies": {
+ "@ethersproject/abstract-provider": "^5.7.0",
+ "@ethersproject/abstract-signer": "^5.7.0",
+ "@ethersproject/address": "^5.7.0",
+ "@ethersproject/base64": "^5.7.0",
+ "@ethersproject/basex": "^5.7.0",
+ "@ethersproject/bignumber": "^5.7.0",
+ "@ethersproject/bytes": "^5.7.0",
+ "@ethersproject/constants": "^5.7.0",
+ "@ethersproject/hash": "^5.7.0",
+ "@ethersproject/logger": "^5.7.0",
+ "@ethersproject/networks": "^5.7.0",
+ "@ethersproject/properties": "^5.7.0",
+ "@ethersproject/random": "^5.7.0",
+ "@ethersproject/rlp": "^5.7.0",
+ "@ethersproject/sha2": "^5.7.0",
+ "@ethersproject/strings": "^5.7.0",
+ "@ethersproject/transactions": "^5.7.0",
+ "@ethersproject/web": "^5.7.0",
+ "bech32": "1.1.4",
+ "ws": "7.4.6"
+ }
+ },
+ "packages/data-fetcher/node_modules/@ethersproject/web": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz",
+ "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.buymeacoffee.com/ricmoo"
+ }
+ ],
+ "dependencies": {
+ "@ethersproject/base64": "^5.7.0",
+ "@ethersproject/bytes": "^5.7.0",
+ "@ethersproject/logger": "^5.7.0",
+ "@ethersproject/properties": "^5.7.0",
+ "@ethersproject/strings": "^5.7.0"
+ }
+ },
+ "packages/data-fetcher/node_modules/ethers": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz",
+ "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.buymeacoffee.com/ricmoo"
+ }
+ ],
+ "dependencies": {
+ "@ethersproject/abi": "5.7.0",
+ "@ethersproject/abstract-provider": "5.7.0",
+ "@ethersproject/abstract-signer": "5.7.0",
+ "@ethersproject/address": "5.7.0",
+ "@ethersproject/base64": "5.7.0",
+ "@ethersproject/basex": "5.7.0",
+ "@ethersproject/bignumber": "5.7.0",
+ "@ethersproject/bytes": "5.7.0",
+ "@ethersproject/constants": "5.7.0",
+ "@ethersproject/contracts": "5.7.0",
+ "@ethersproject/hash": "5.7.0",
+ "@ethersproject/hdnode": "5.7.0",
+ "@ethersproject/json-wallets": "5.7.0",
+ "@ethersproject/keccak256": "5.7.0",
+ "@ethersproject/logger": "5.7.0",
+ "@ethersproject/networks": "5.7.1",
+ "@ethersproject/pbkdf2": "5.7.0",
+ "@ethersproject/properties": "5.7.0",
+ "@ethersproject/providers": "5.7.2",
+ "@ethersproject/random": "5.7.0",
+ "@ethersproject/rlp": "5.7.0",
+ "@ethersproject/sha2": "5.7.0",
+ "@ethersproject/signing-key": "5.7.0",
+ "@ethersproject/solidity": "5.7.0",
+ "@ethersproject/strings": "5.7.0",
+ "@ethersproject/transactions": "5.7.0",
+ "@ethersproject/units": "5.7.0",
+ "@ethersproject/wallet": "5.7.0",
+ "@ethersproject/web": "5.7.1",
+ "@ethersproject/wordlists": "5.7.0"
+ }
+ },
+ "packages/data-fetcher/node_modules/ws": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+ "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "packages/data-fetcher/node_modules/zksync-web3": {
+ "version": "0.15.4",
+ "resolved": "https://registry.npmjs.org/zksync-web3/-/zksync-web3-0.15.4.tgz",
+ "integrity": "sha512-6CEpRBbF4nGwRYSF3KvPGqg2aNJFYTl8AR+cejBnC2Uyu1v3NYSkmkXXVuMGupJ7HIQR1aTqFEDsUFPyO/bL0Q==",
+ "deprecated": "This package has been deprecated in favor of zksync-ethers@5.0.0",
+ "peerDependencies": {
+ "ethers": "^5.7.0"
+ }
+ },
"packages/integration-tests": {
"version": "0.0.0",
"hasInstallScript": true,
diff --git a/package.json b/package.json
index 66f4518ed2..811b4cc761 100644
--- a/package.json
+++ b/package.json
@@ -16,8 +16,8 @@
"test": "lerna run test",
"test:ci": "lerna run test:ci",
"test:e2e": "lerna run test:e2e",
- "test:integration:ui": "lerna run integration-test:ui",
- "test:integration:api": "lerna run integration-test:api",
+ "test:integration:ui": "lerna run integration-tests:ui",
+ "test:integration:api": "lerna run integration-tests:api",
"dev": "lerna run dev",
"build": "lerna run build",
"start": "lerna run start",
diff --git a/packages/api/src/api/api.controller.ts b/packages/api/src/api/api.controller.ts
index 4bb82d2d4b..ccd2bb5ec8 100644
--- a/packages/api/src/api/api.controller.ts
+++ b/packages/api/src/api/api.controller.ts
@@ -609,7 +609,10 @@ export class ApiController {
@ApiTags("Token API")
@Get("api?module=token&action=tokeninfo")
- @ApiOperation({ summary: "Returns token information" })
+ @ApiOperation({
+ summary:
+ "Returns token information. Token price, liquidity and icon are retrieved from CoinGecko. The data is updated every 24 hours.",
+ })
@ApiQuery({
name: "contractaddress",
description: "The contract address of the ERC-20/ERC-721 token to retrieve token info",
diff --git a/packages/app/README.md b/packages/app/README.md
index 1023fc9388..692aeb3de6 100644
--- a/packages/app/README.md
+++ b/packages/app/README.md
@@ -46,7 +46,6 @@ const config: EnvironmentConfig = {
icon: "/images/icons/zksync-arrows.svg",
l2ChainId: 270,
l2NetworkName: "Local",
- l2WalletUrl: "https://goerli.staging-portal.zksync.dev/",
maintenance: false,
name: "local",
published: true,
@@ -60,7 +59,6 @@ const config: EnvironmentConfig = {
icon: "/images/icons/zksync-arrows.svg",
l2ChainId: 270,
l2NetworkName: "Local Hyperchain",
- l2WalletUrl: "https://goerli.staging-portal.zksync.dev/",
maintenance: false,
name: "local-hyperchain",
published: true,
@@ -106,3 +104,13 @@ npm run lint
## Production links
- [Web Application](https://explorer.zksync.io)
- [Storybook](https://storybook-scan-v2.zksync.dev)
+
+
+## Verify Block Explorer UI test results in GitHub Actions
+GitHub Actions test results are available in:
+
+- `GitHub Actions` --> `Summary` page at the very end of a page.
+- Inside of each test run in the log: `Feature on Mainnet + Sepolia` --> `@search` --> `Upload test results to Allure reporter` --> `https://raw.githack.com/matter-labs/block-explorer/gh-pages/_github.run_number_/index.html`
+- Directly via a link `https://raw.githack.com/matter-labs/block-explorer/gh-pages/_github.run_number_/index.html` after each PR running. The history of test runs for public view locates in `gh-pages` branch.
+
+In case of 404 page, make sure that the folder with its `github.run_number` exists in the `gh-pages`. If the folder exist, try again in a few minutes as `https://raw.githack.com` needs to update the data. Public report link will be available when the 'Allure Report' job will be succesfully executed.
diff --git a/packages/app/src/App.vue b/packages/app/src/App.vue
index a2541a6834..f6e453715a 100644
--- a/packages/app/src/App.vue
+++ b/packages/app/src/App.vue
@@ -2,6 +2,8 @@
+
+
@@ -12,6 +14,8 @@
diff --git a/packages/app/src/components/NetworkDeprecated.vue b/packages/app/src/components/NetworkDeprecated.vue
new file mode 100644
index 0000000000..de32dc159d
--- /dev/null
+++ b/packages/app/src/components/NetworkDeprecated.vue
@@ -0,0 +1,41 @@
+
+
+ We are ending our support of Goerli testnet. Please use Sepolia . For more info see
+ this announcement .
+
+
+
+
+
+
diff --git a/packages/app/src/components/TheFooter.vue b/packages/app/src/components/TheFooter.vue
index 86b0c44590..8a3ad69b09 100644
--- a/packages/app/src/components/TheFooter.vue
+++ b/packages/app/src/components/TheFooter.vue
@@ -24,7 +24,7 @@ const config = useRuntimeConfig();
const navigation = reactive([
{
label: computed(() => t("footer.nav.docs")),
- url: "https://era.zksync.io/docs/dev/",
+ url: "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html",
},
{
label: computed(() => t("footer.nav.terms")),
diff --git a/packages/app/src/components/common/CheckBoxInput.stories.ts b/packages/app/src/components/common/CheckBoxInput.stories.ts
new file mode 100644
index 0000000000..6f1d6a20fc
--- /dev/null
+++ b/packages/app/src/components/common/CheckBoxInput.stories.ts
@@ -0,0 +1,29 @@
+import CheckBoxInput from "./CheckBoxInput.vue";
+
+export default {
+ title: "Common/CheckBoxInput",
+ component: CheckBoxInput,
+};
+
+type Args = {
+ modelValue: boolean;
+};
+
+const Template = (args: Args) => ({
+ components: { CheckBoxInput },
+ setup() {
+ return { args };
+ },
+ template: `
+ CheckBox Input `,
+});
+
+export const Checked = Template.bind({}) as unknown as { args: Args };
+Checked.args = {
+ modelValue: true,
+};
+
+export const Unchecked = Template.bind({}) as unknown as { args: Args };
+Unchecked.args = {
+ modelValue: false,
+};
diff --git a/packages/app/src/components/common/CheckBoxInput.vue b/packages/app/src/components/common/CheckBoxInput.vue
new file mode 100644
index 0000000000..04f1de75b6
--- /dev/null
+++ b/packages/app/src/components/common/CheckBoxInput.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/app/src/components/common/SystemAlert.vue b/packages/app/src/components/common/SystemAlert.vue
new file mode 100644
index 0000000000..4dd6e3c059
--- /dev/null
+++ b/packages/app/src/components/common/SystemAlert.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/app/src/components/form/FormItem.vue b/packages/app/src/components/form/FormItem.vue
index 8e179ae1be..f5a72f24e3 100644
--- a/packages/app/src/components/form/FormItem.vue
+++ b/packages/app/src/components/form/FormItem.vue
@@ -42,7 +42,7 @@ defineProps({
}
.label-inline-block {
.form-item-label {
- @apply inline-block;
+ @apply float-left;
}
}
diff --git a/packages/app/src/components/header/TheHeader.vue b/packages/app/src/components/header/TheHeader.vue
index 34f96b357e..dd75815f1d 100644
--- a/packages/app/src/components/header/TheHeader.vue
+++ b/packages/app/src/components/header/TheHeader.vue
@@ -48,7 +48,11 @@
-
+
t("header.nav.documentation")),
- url: "https://era.zksync.io/docs/dev/",
+ url: "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html",
},
]);
@@ -187,10 +191,6 @@ const links = [
label: computed(() => t("header.nav.contractVerification")),
to: { name: "contract-verification" },
},
- {
- label: computed(() => t("header.nav.portal")),
- url: computed(() => currentNetwork.value.l2WalletUrl),
- },
];
if (currentNetwork.value.bridgeUrl) {
@@ -301,12 +301,20 @@ const hasContent = computed(() => {
.hero-banner-container {
@apply absolute left-0 top-full flex h-64 w-full items-end justify-end overflow-hidden bg-primary-900;
+ &.goerli {
+ @apply h-[25rem] md:h-[23rem] lg:h-[19rem];
+ }
+
.hero-image {
@apply h-5/6 w-auto;
}
}
.home-banner {
@apply h-80;
+
+ &.goerli {
+ @apply h-[30rem] md:h-[27rem] lg:h-[24rem];
+ }
}
}
.header-mobile-popover {
diff --git a/packages/app/src/components/transactions/Status.vue b/packages/app/src/components/transactions/Status.vue
index 9fb3172d57..1491ba28af 100644
--- a/packages/app/src/components/transactions/Status.vue
+++ b/packages/app/src/components/transactions/Status.vue
@@ -249,16 +249,6 @@ const badges = computed(() => {
});
return badgesArr;
}
- if (props.status === "indexing") {
- badgesArr.push({
- testId: "indexing",
- color: "neutral",
- text: t("transactions.statusComponent.indexing"),
- infoTooltip: t("transactions.statusComponent.indexingTooltip"),
- icon: Spinner,
- });
- return badgesArr;
- }
badgesArr.push({
testId: "l2-badge-title",
@@ -273,6 +263,16 @@ const badges = computed(() => {
icon: CheckIcon,
});
+ if (props.status === "indexing") {
+ badgesArr.push({
+ testId: "indexing",
+ color: "neutral",
+ text: t("transactions.statusComponent.indexing"),
+ icon: Spinner,
+ });
+ return badgesArr;
+ }
+
badgesArr.push({
testId: "l1-badge-title",
color: props.status === "verified" ? "success" : "neutral",
diff --git a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue
index f1b554ed4a..40fbb238d6 100644
--- a/packages/app/src/components/transactions/infoTable/GeneralInfo.vue
+++ b/packages/app/src/components/transactions/infoTable/GeneralInfo.vue
@@ -30,7 +30,7 @@
/>
-
+
{{ t("transactions.table.reason") }}
diff --git a/packages/app/src/composables/useRuntimeConfig.ts b/packages/app/src/composables/useRuntimeConfig.ts
index b0a053ee8e..0e7e6dc32c 100644
--- a/packages/app/src/composables/useRuntimeConfig.ts
+++ b/packages/app/src/composables/useRuntimeConfig.ts
@@ -3,13 +3,12 @@ import type { NetworkConfig, RuntimeConfig } from "@/configs";
export const DEFAULT_NETWORK: NetworkConfig = {
apiUrl: "https://block-explorer-api.testnets.zksync.dev",
verificationApiUrl: "https://zksync2-testnet-explorer.zksync.dev",
- bridgeUrl: "https://goerli.bridge.zksync.io",
+ bridgeUrl: "https://portal.zksync.io/bridge/?network=goerli",
hostnames: ["https://goerli.explorer.zksync.io"],
icon: "/images/icons/zksync-arrows.svg",
l1ExplorerUrl: "https://goerli.etherscan.io",
l2ChainId: 280,
l2NetworkName: "zkSync Era Goerli Testnet",
- l2WalletUrl: "https://goerli.portal.zksync.io/",
maintenance: false,
name: "goerli",
published: true,
diff --git a/packages/app/src/composables/useSearch.ts b/packages/app/src/composables/useSearch.ts
index 97fd55c685..1f8e0daac7 100644
--- a/packages/app/src/composables/useSearch.ts
+++ b/packages/app/src/composables/useSearch.ts
@@ -20,18 +20,21 @@ export default (context = useContext()) => {
apiRoute: "address",
isValid: () => isAddress(param),
routeName: "address",
+ prefetch: true,
},
{
routeParam: { id: param },
apiRoute: "batches",
isValid: () => isBlockNumber(param),
routeName: "batch",
+ prefetch: true,
},
{
routeParam: { hash: param },
apiRoute: "transactions",
isValid: () => isTransactionHash(param),
routeName: "transaction",
+ prefetch: false,
},
];
@@ -46,7 +49,9 @@ export default (context = useContext()) => {
const searchRoute = getSearchRoute(param);
if (searchRoute) {
try {
- await $fetch(`${context.currentNetwork.value.apiUrl}/${searchRoute.apiRoute}/${param}`);
+ if (searchRoute.prefetch) {
+ await $fetch(`${context.currentNetwork.value.apiUrl}/${searchRoute.apiRoute}/${param}`);
+ }
await router.push({ name: searchRoute.routeName, params: searchRoute.routeParam });
return;
} catch (error) {
diff --git a/packages/app/src/configs/dev.config.json b/packages/app/src/configs/dev.config.json
index 07de06fbd1..ceca3f98f7 100644
--- a/packages/app/src/configs/dev.config.json
+++ b/packages/app/src/configs/dev.config.json
@@ -9,7 +9,6 @@
"icon": "/images/icons/zksync-arrows.svg",
"l2ChainId": 270,
"l2NetworkName": "Local",
- "l2WalletUrl": "http://localhost:3000",
"maintenance": false,
"name": "local",
"published": true,
@@ -18,7 +17,7 @@
{
"apiUrl": "https://block-explorer-api.testnets.zksync.dev",
"verificationApiUrl": "https://zksync2-testnet-explorer.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=goerli",
"hostnames": [
"https://goerli.staging-scan-v2.zksync.dev"
],
@@ -26,7 +25,6 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 280,
"l2NetworkName": "zkSync Era Goerli Testnet",
- "l2WalletUrl": "https://goerli.staging-portal.zksync.dev/",
"maintenance": false,
"name": "goerli",
"published": true,
@@ -35,13 +33,12 @@
{
"apiUrl": "https://block-explorer-api.sepolia.zksync.dev",
"verificationApiUrl": "https://explorer.sepolia.era.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=sepolia",
"hostnames": [],
"icon": "/images/icons/zksync-arrows.svg",
"l1ExplorerUrl": "https://sepolia.etherscan.io",
"l2ChainId": 300,
"l2NetworkName": "zkSync Era Sepolia Testnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/?network=era-boojnet",
"maintenance": false,
"name": "sepolia",
"published": true,
@@ -57,7 +54,6 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 270,
"l2NetworkName": "Goerli (Stage2)",
- "l2WalletUrl": "https://goerli-beta.staging-portal.zksync.dev/",
"maintenance": false,
"name": "goerli-beta",
"published": true,
@@ -66,7 +62,7 @@
{
"apiUrl": "https://block-explorer-api.mainnet.zksync.io",
"verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io",
- "bridgeUrl": "https://staging.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=mainnet",
"hostnames": [
"https://staging-scan-v2.zksync.dev"
],
@@ -74,7 +70,6 @@
"l1ExplorerUrl": "https://etherscan.io",
"l2ChainId": 324,
"l2NetworkName": "zkSync Era Mainnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/",
"maintenance": false,
"name": "mainnet",
"published": true,
diff --git a/packages/app/src/configs/index.ts b/packages/app/src/configs/index.ts
index edee2ceeae..22b59f9657 100644
--- a/packages/app/src/configs/index.ts
+++ b/packages/app/src/configs/index.ts
@@ -6,7 +6,6 @@ export type NetworkConfig = {
rpcUrl: string;
bridgeUrl?: string;
l2NetworkName: string;
- l2WalletUrl: string;
l2ChainId: 270 | 280 | 324;
l1ExplorerUrl?: string;
maintenance: boolean;
diff --git a/packages/app/src/configs/local.config.json b/packages/app/src/configs/local.config.json
index afd0e82298..cbaee29f8a 100644
--- a/packages/app/src/configs/local.config.json
+++ b/packages/app/src/configs/local.config.json
@@ -9,7 +9,6 @@
"icon": "/images/icons/zksync-arrows.svg",
"l2ChainId": 270,
"l2NetworkName": "Local",
- "l2WalletUrl": "http://localhost:3000",
"maintenance": false,
"name": "local",
"published": true,
diff --git a/packages/app/src/configs/production.config.json b/packages/app/src/configs/production.config.json
index d2a5f9eab7..066884be9c 100644
--- a/packages/app/src/configs/production.config.json
+++ b/packages/app/src/configs/production.config.json
@@ -3,7 +3,7 @@
{
"apiUrl": "https://block-explorer-api.testnets.zksync.dev",
"verificationApiUrl": "https://zksync2-testnet-explorer.zksync.dev",
- "bridgeUrl": "https://goerli.bridge.zksync.io",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=goerli",
"hostnames": [
"https://goerli.explorer.zksync.io"
],
@@ -11,7 +11,6 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 280,
"l2NetworkName": "zkSync Era Goerli Testnet",
- "l2WalletUrl": "https://goerli.portal.zksync.io/",
"maintenance": false,
"name": "goerli",
"published": true,
@@ -20,7 +19,7 @@
{
"apiUrl": "https://block-explorer-api.sepolia.zksync.dev",
"verificationApiUrl": "https://explorer.sepolia.era.zksync.dev",
- "bridgeUrl": "https://bridge.zksync.io",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=sepolia",
"hostnames": [
"https://sepolia.explorer.zksync.io"
],
@@ -28,7 +27,6 @@
"l1ExplorerUrl": "https://sepolia.etherscan.io",
"l2ChainId": 300,
"l2NetworkName": "zkSync Era Sepolia Testnet",
- "l2WalletUrl": "https://portal.zksync.io/",
"maintenance": false,
"name": "sepolia",
"published": true,
@@ -37,7 +35,7 @@
{
"apiUrl": "https://block-explorer-api.mainnet.zksync.io",
"verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io",
- "bridgeUrl": "https://bridge.zksync.io",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=mainnet",
"hostnames": [
"https://explorer.zksync.io"
],
@@ -45,7 +43,6 @@
"l1ExplorerUrl": "https://etherscan.io",
"l2ChainId": 324,
"l2NetworkName": "zkSync Era Mainnet",
- "l2WalletUrl": "https://portal.zksync.io/",
"maintenance": false,
"name": "mainnet",
"published": true,
diff --git a/packages/app/src/configs/staging.config.json b/packages/app/src/configs/staging.config.json
index fe9e8d4c27..fd3818f4b2 100644
--- a/packages/app/src/configs/staging.config.json
+++ b/packages/app/src/configs/staging.config.json
@@ -3,7 +3,7 @@
{
"apiUrl": "https://block-explorer-api.testnets.zksync.dev",
"verificationApiUrl": "https://zksync2-testnet-explorer.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=goerli",
"hostnames": [
"https://goerli.staging-scan-v2.zksync.dev"
],
@@ -11,7 +11,6 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 280,
"l2NetworkName": "zkSync Era Goerli Testnet",
- "l2WalletUrl": "https://goerli.staging-portal.zksync.dev/",
"maintenance": false,
"name": "goerli",
"published": true,
@@ -20,7 +19,7 @@
{
"apiUrl": "https://block-explorer-api.sepolia.zksync.dev",
"verificationApiUrl": "https://explorer.sepolia.era.zksync.dev",
- "bridgeUrl": "https://staging.goerli.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=sepolia",
"hostnames": [
"https://sepolia.staging-scan-v2.zksync.dev"
],
@@ -28,7 +27,6 @@
"l1ExplorerUrl": "https://sepolia.etherscan.io",
"l2ChainId": 300,
"l2NetworkName": "zkSync Era Sepolia Testnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/?network=era-boojnet",
"maintenance": false,
"name": "sepolia",
"published": true,
@@ -44,7 +42,6 @@
"l1ExplorerUrl": "https://goerli.etherscan.io",
"l2ChainId": 270,
"l2NetworkName": "Goerli (Stage2)",
- "l2WalletUrl": "https://goerli-beta.staging-portal.zksync.dev/",
"maintenance": false,
"name": "goerli-beta",
"published": true,
@@ -53,7 +50,7 @@
{
"apiUrl": "https://block-explorer-api.mainnet.zksync.io",
"verificationApiUrl": "https://zksync2-mainnet-explorer.zksync.io",
- "bridgeUrl": "https://staging.bridge.zksync.dev",
+ "bridgeUrl": "https://portal.zksync.io/bridge/?network=mainnet",
"hostnames": [
"https://staging-scan-v2.zksync.dev"
],
@@ -61,7 +58,6 @@
"l1ExplorerUrl": "https://etherscan.io",
"l2ChainId": 324,
"l2NetworkName": "zkSync Era Mainnet",
- "l2WalletUrl": "https://staging-portal.zksync.dev/",
"maintenance": false,
"name": "mainnet",
"published": true,
diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json
index f40c91c7ea..e4b231daf5 100644
--- a/packages/app/src/locales/en.json
+++ b/packages/app/src/locales/en.json
@@ -189,7 +189,7 @@
},
"statusComponent": {
"processed": "Processed",
- "indexing": "Indexing...",
+ "indexing": "Being indexed by explorer",
"sending": "Sending",
"sent": "Sent",
"validating": "Validating",
@@ -197,7 +197,6 @@
"executing": "Executing",
"executed": "Executed",
"failed": "Failed",
- "indexingTooltip": "Transaction is indexing and will be fully reflected shortly",
"ethereumNetwork": "Ethereum network"
},
"transaction": "Transaction",
@@ -282,7 +281,6 @@
"header": {
"nav": {
"blockExplorer": "Block Explorer",
- "portal": "Portal",
"documentation": "Documentation",
"tools": "Tools",
"apiDocs": "API Documentation",
@@ -459,7 +457,8 @@
"validation": {
"required": "Solc version is required"
},
- "error": "Unable to get list of supported Solc versions"
+ "error": "Unable to get list of supported Solc versions",
+ "zkVM": "zkVM"
},
"zksolcVersion": {
"label": "Zksolc Version",
@@ -722,5 +721,9 @@
"callData": "calldata"
}
}
+ },
+ "systemAlert": {
+ "indexerDelayed": "Transaction indexing is {indexerDelayInHours} hours behind. Transactions are being processed normally and will gradually show up. You can also use other explorers meanwhile.",
+ "indexerDelayedDueToHeavyLoad": "The network is under a heavy load at the moment and transaction indexing on the explorer is {indexerDelayInHours} hours behind. Transactions are being processed normally and will gradually show up. You can also use other explorers meanwhile."
}
}
\ No newline at end of file
diff --git a/packages/app/src/locales/uk.json b/packages/app/src/locales/uk.json
index 9ff43d706f..c8b946f0fa 100644
--- a/packages/app/src/locales/uk.json
+++ b/packages/app/src/locales/uk.json
@@ -128,7 +128,7 @@
},
"statusComponent": {
"processed": "Оброблено",
- "indexing": "Індексується...",
+ "indexing": "Індексується експлорером",
"sending": "Надсилається",
"sent": "Надіслано",
"validating": "Перевіряється",
@@ -136,7 +136,6 @@
"executing": "Виконується",
"executed": "Виконано",
"failed": "Невдало",
- "indexingTooltip": "Транзакція індексується, всі деталі будуть доступні згодом",
"ethereumNetwork": "мережа Ethereum"
},
"transaction": "Транзакція",
@@ -153,7 +152,6 @@
"contractVerification": "Верифікація Смарт контракту",
"debugger": "zkEVM Налагоджувач",
"blockExplorer": "Провідник",
- "portal": "Портал",
"documentation": "Документація"
}
},
@@ -439,5 +437,9 @@
"callData": "дані виклику"
}
}
+ },
+ "systemAlert": {
+ "indexerDelayed": "Індексація транзакцій відстає на {indexerDelayInHours} годин. Транзакції будуть поступово оброблені та відображені. Ви також можете скористатися іншими блок експлорерами наразі.",
+ "indexerDelayedDueToHeavyLoad": "Мережа наразі перебуває під великим навантаженням, індексація транзакцій відстає на {indexerDelayInHours} годин. Транзакції будуть поступово оброблені та відображені. Ви також можете скористатися іншими блок експлорерами наразі."
}
}
diff --git a/packages/app/src/views/ContractVerificationView.vue b/packages/app/src/views/ContractVerificationView.vue
index f6581a0b5a..2bc9272bfa 100644
--- a/packages/app/src/views/ContractVerificationView.vue
+++ b/packages/app/src/views/ContractVerificationView.vue
@@ -16,7 +16,7 @@
-
+
{{ t("contractVerification.resources.links.hardhat") }}
@@ -84,12 +84,13 @@
class="label-inline-block"
>
{{ t(`contractVerification.form.${selectedZkCompiler.name}Version.details`) }}
-
+
+ {{ t("contractVerification.form.solcVersion.zkVM") }}
[
},
]);
+const isZkVMSolcCompiler = ref(false);
const selectedCompilationType = ref(CompilationTypeOptionsEnum.soliditySingleFile);
const isSingleFile = computed(() =>
[CompilationTypeOptionsEnum.soliditySingleFile, CompilationTypeOptionsEnum.vyperSingleFile].includes(
@@ -350,7 +365,16 @@ const selectedZkCompiler = computed(() => {
});
const selectedCompiler = computed(() => {
const compiler = compilerTypeMap[selectedCompilationType.value].compiler;
- return compilerVersions.value[compiler];
+ const compilerInfo = compilerVersions.value[compiler];
+ if (compiler === CompilerEnum.solc) {
+ return {
+ ...compilerInfo,
+ versions: compilerInfo.versions?.filter((version) =>
+ isZkVMSolcCompiler.value ? version.startsWith(zkVMVersionPrefix) : !version.startsWith(zkVMVersionPrefix)
+ ),
+ };
+ }
+ return compilerInfo;
});
const selectedZkCompilerVersion = ref(
selectedZkCompiler.value.versions[selectedZkCompiler.value.versions.length - 1] || ""
@@ -496,6 +520,10 @@ const v$ = useVuelidate(
form
);
+function onZkVMSelectionChanged() {
+ selectedCompilerVersion.value = selectedCompiler.value.versions[0] || "";
+}
+
function onCompilationTypeChange() {
selectedZkCompilerVersion.value = selectedZkCompiler.value.versions[0] || "";
selectedCompilerVersion.value = selectedCompiler.value.versions[0] || "";
diff --git a/packages/app/tests/components/FeeData.spec.ts b/packages/app/tests/components/FeeData.spec.ts
index 94e9e713e8..c12493e2ca 100644
--- a/packages/app/tests/components/FeeData.spec.ts
+++ b/packages/app/tests/components/FeeData.spec.ts
@@ -189,7 +189,7 @@ describe("FeeToken", () => {
await fireEvent.click(container.querySelector(".toggle-button")!);
const link = container.querySelector(".refunded-link");
expect(link?.getAttribute("href")).toBe(
- "https://era.zksync.io/docs/dev/developer-guides/transactions/fee-model.html#refunds"
+ "https://docs.zksync.io/build/developer-reference/fee-model.html#refunds"
);
expect(link?.getAttribute("target")).toBe("_blank");
expect(link?.textContent).toBe("Why am I being refunded?");
@@ -240,14 +240,14 @@ describe("FeeToken", () => {
const refundedLink = container.querySelector(".refunded-link");
expect(refundedLink?.getAttribute("href")).toBe(
- "https://era.zksync.io/docs/dev/developer-guides/transactions/fee-model.html#refunds"
+ "https://docs.zksync.io/build/developer-reference/fee-model.html#refunds"
);
expect(refundedLink?.getAttribute("target")).toBe("_blank");
expect(refundedLink?.textContent).toBe("Why is Paymaster being refunded?");
const paymasterLink = container.querySelector(".paymaster-link");
expect(paymasterLink?.getAttribute("href")).toBe(
- "https://era.zksync.io/docs/reference/concepts/account-abstraction.html#paymasters"
+ "https://docs.zksync.io/build/developer-reference/account-abstraction.html#paymasters"
);
expect(paymasterLink?.getAttribute("target")).toBe("_blank");
expect(paymasterLink?.textContent).toBe("What is Paymaster?");
diff --git a/packages/app/tests/components/TheFooter.spec.ts b/packages/app/tests/components/TheFooter.spec.ts
index b799dc76a7..8ecf1a640d 100644
--- a/packages/app/tests/components/TheFooter.spec.ts
+++ b/packages/app/tests/components/TheFooter.spec.ts
@@ -24,7 +24,9 @@ describe("TheFooter:", () => {
},
});
const links = wrapper.findAll("a");
- expect(links[0].attributes("href")).toBe("https://era.zksync.io/docs/dev/");
+ expect(links[0].attributes("href")).toBe(
+ "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html"
+ );
expect(links[1].attributes("href")).toBe("https://zksync.io/terms");
expect(links[2].attributes("href")).toBe("https://zksync.io/contact");
});
diff --git a/packages/app/tests/components/TheHeader.spec.ts b/packages/app/tests/components/TheHeader.spec.ts
index 693806228c..a5367a5832 100644
--- a/packages/app/tests/components/TheHeader.spec.ts
+++ b/packages/app/tests/components/TheHeader.spec.ts
@@ -23,7 +23,6 @@ vi.mock("@/composables/useContext", () => {
default: () => ({
currentNetwork: computed(() => ({
maintenance: maintenanceMock(),
- l2WalletUrl: "https://portal.zksync.io/",
bridgeUrl: "https://bridge.zksync.io/",
apiUrl: "https://api-url",
})),
@@ -60,10 +59,10 @@ describe("TheHeader:", () => {
const toolsLinks = dropdown[1].findAll("a");
expect(toolsLinks[0].attributes("href")).toBe("https://api-url/docs");
expect(toolsLinksRouter[0].props().to.name).toBe("contract-verification");
- expect(toolsLinks[2].attributes("href")).toBe("https://portal.zksync.io/");
+ expect(toolsLinks[2].attributes("href")).toBe("https://bridge.zksync.io/");
expect(wrapper.findAll(".navigation-container > .navigation-link")[0].attributes("href")).toBe(
- "https://era.zksync.io/docs/dev/"
+ "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html"
);
});
it("renders social links", () => {
diff --git a/packages/app/tests/components/common/CheckBoxInput.spec.ts b/packages/app/tests/components/common/CheckBoxInput.spec.ts
new file mode 100644
index 0000000000..f51e052269
--- /dev/null
+++ b/packages/app/tests/components/common/CheckBoxInput.spec.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+
+import { render } from "@testing-library/vue";
+
+import CheckBoxInput from "@/components/common/CheckBoxInput.vue";
+
+describe("CheckBoxInput", () => {
+ it("renders default slot", () => {
+ const { container } = render(CheckBoxInput, {
+ slots: {
+ default: {
+ template: "CheckBox Input",
+ },
+ },
+ props: {
+ modelValue: true,
+ },
+ });
+ expect(container.textContent).toBe("CheckBox Input");
+ });
+ it("renders checked state correctly", async () => {
+ const { container } = render(CheckBoxInput, {
+ props: {
+ modelValue: true,
+ },
+ });
+ expect(container.querySelector(".checkbox-input-container")!.classList.contains("checked")).toBe(true);
+ expect(container.querySelector(".checkbox-input-container input")?.checked).toBe(true);
+ });
+ it("renders unchecked state correctly", async () => {
+ const { container } = render(CheckBoxInput, {
+ props: {
+ modelValue: false,
+ },
+ });
+ expect(container.querySelector(".checkbox-input-container")!.classList.contains("checked")).toBe(false);
+ expect(container.querySelector(".checkbox-input-container input")?.checked).toBe(false);
+ });
+});
diff --git a/packages/app/tests/components/transactions/GeneralInfo.spec.ts b/packages/app/tests/components/transactions/GeneralInfo.spec.ts
index aaf3bdd00c..63450cbd36 100644
--- a/packages/app/tests/components/transactions/GeneralInfo.spec.ts
+++ b/packages/app/tests/components/transactions/GeneralInfo.spec.ts
@@ -305,7 +305,7 @@ describe("Transaction info table", () => {
expect(nonceTooltip).toBe(i18n.global.t("transactions.table.nonceTooltip"));
expect(createdAtTooltip).toBe(i18n.global.t("transactions.table.createdTooltip"));
});
- it("renders indexing transaction status and tooltip", async () => {
+ it("renders indexing transaction status", async () => {
const wrapper = mount(Table, {
global: {
stubs: {
@@ -322,10 +322,11 @@ describe("Transaction info table", () => {
await nextTick();
const status = wrapper.findAll("tbody tr td:nth-child(2)")[1];
const badges = status.findAllComponents(Badge);
- expect(badges.length).toBe(1);
- expect(badges[0].text()).toBe(i18n.global.t("transactions.statusComponent.indexing"));
- const indexingTooltip = wrapper.find(".transaction-status .info-tooltip");
- expect(indexingTooltip.text()).toBe(i18n.global.t("transactions.statusComponent.indexingTooltip"));
+
+ const [l2StatusBadgeTitle, l2StatusBadgeValue, indexingBadge] = badges;
+ expect(l2StatusBadgeTitle.text()).toBe(i18n.global.t("general.l2NetworkName"));
+ expect(l2StatusBadgeValue.text()).toBe(i18n.global.t("transactions.statusComponent.processed"));
+ expect(indexingBadge.text()).toBe(i18n.global.t("transactions.statusComponent.indexing"));
});
it("renders failed transaction status", async () => {
const wrapper = mount(Table, {
diff --git a/packages/app/tests/components/transactions/Status.spec.ts b/packages/app/tests/components/transactions/Status.spec.ts
index 82f3c72e77..9fe7d16e27 100644
--- a/packages/app/tests/components/transactions/Status.spec.ts
+++ b/packages/app/tests/components/transactions/Status.spec.ts
@@ -6,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vite
import { mount } from "@vue/test-utils";
import Badge from "@/components/common/Badge.vue";
-import InfoTooltip from "@/components/common/InfoTooltip.vue";
import Spinner from "@/components/common/Spinner.vue";
import Status from "@/components/transactions/Status.vue";
@@ -350,7 +349,7 @@ describe("Status", () => {
expect(l1ExecutedLink.attributes("href")).toBeUndefined();
});
});
- it("shows icon tooltip and single indexing badge for 'indexing' status", async () => {
+ it("shows indexing badge for 'indexing' status", async () => {
const wrapper = mount(Status, {
global,
props: {
@@ -362,15 +361,18 @@ describe("Status", () => {
});
const badges = wrapper.findAllComponents(Badge);
- expect(badges.length).toBe(1);
+ expect(badges.length).toBe(3);
+
+ const [l2StatusBadgeTitle, l2StatusBadgeValue, indexingBadge] = badges;
- const [indexingBadge] = badges;
+ expect(l2StatusBadgeTitle.text()).toBe(i18n.global.t("general.l2NetworkName"));
+ expect(l2StatusBadgeTitle.props().color).toBe("success");
+ expect(l2StatusBadgeTitle.props().textColor).toBe("neutral");
+
+ expect(l2StatusBadgeValue.text()).toBe(i18n.global.t("transactions.statusComponent.processed"));
+ expect(l2StatusBadgeValue.props().color).toBe("dark-success");
expect(indexingBadge.props().color).toBe("neutral");
expect(indexingBadge.text()).toBe(i18n.global.t("transactions.statusComponent.indexing"));
-
- const infoTooltip = wrapper.findAllComponents(InfoTooltip);
- expect(infoTooltip.length).toBe(1);
- expect(infoTooltip[0].text()).toBe(i18n.global.t("transactions.statusComponent.indexingTooltip"));
});
});
diff --git a/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature b/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature
index 4b1cb560f2..f985c01e6b 100644
--- a/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature
+++ b/packages/app/tests/e2e/features/artifacts/artifactsSet1.feature
@@ -4,53 +4,30 @@ Feature: Main Page
Background:
Given I am on main page
- @id253 @featureEnv @testnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
- Given I click by text "Tools"
- Given Element with "text" "" should be "visible"
- When Element with "text" "" should be "clickable"
- Then Element with "text" "" should have "" value
-
- Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://goerli.staging-portal.zksync.dev/ |
-
- @id253 @featureEnv @mainnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
- Given I click by text "Tools"
- Given Element with "text" "" should be "visible"
- When Element with "text" "" should be "clickable"
- Then Element with "text" "" should have "" value
- Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://staging-portal.zksync.dev/ |
-
- @id253:I @productionEnv @testnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
+ @id253:I @featureEnv @testnetSmokeSuite @testnet
+ Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href (Sepolia)
Given I click by text "Tools"
Given Element with "text" "" should be "visible"
When Element with "text" "" should be "clickable"
Then Element with "text" "" should have "" value
Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://goerli.portal.zksync.io/ |
+ | Sub-Section | url |
+ | Smart Contract Verification | /contracts/verify |
+ | Bridge | https://portal.zksync.io/bridge/?network=sepolia |
- @id253:I @productionEnv @mainnet
- Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href
+ @id253:II @mainnet
+ Scenario Outline: Check the element "" in Tools section is available, clickable and have correct href (Mainnet)
Given I click by text "Tools"
Given Element with "text" "" should be "visible"
When Element with "text" "" should be "clickable"
Then Element with "text" "" should have "" value
Examples:
- | Sub-Section | url |
- | Smart Contract Verification | /contracts/verify |
- | Portal | https://portal.zksync.io/ |
+ | Sub-Section | url |
+ | Smart Contract Verification | /contracts/verify |
+ | Bridge | https://portal.zksync.io/bridge/?network=mainnet |
@id231
Scenario Outline: Check social networks icon "" is available, clickable and have correct href
@@ -85,7 +62,7 @@ Feature: Main Page
| zkSync Era Sepolia Testnet | network |
| zkSync Era Goerli Testnet | network |
| Goerli (Stage2) | network |
-
+
@id254:II @productionEnv
Scenario Outline: Check dropdown "" for "" and verify
Given Set the "" value for "" switcher
@@ -139,7 +116,7 @@ Feature: Main Page
Given I go to page "/address/0x8f0F33583a56908F7F933cd6F0AaE382aC3fd8f6"
Then Element with "id" "search" should be "visible"
- @id209:I @testnet
+ @id209:I @testnet
Scenario Outline: Verify Transaction table contains "" row
Given I go to page "/tx/0xe7a91cc9b270d062328ef995e0ef67195a3703d43ce4e1d375f87d5c64e51981"
When Table contains row with ""
@@ -201,7 +178,7 @@ Feature: Main Page
| Created | 2023-05-14 |
- @id211 @testnet
+ @id211 @testnet
Scenario Outline: Verify Contract info table contains "" row
Given I go to page "/address/0x3e7676937A7E96CFB7616f255b9AD9FF47363D4b"
Then Element with "text" "" should be "visible"
diff --git a/packages/app/tests/e2e/features/redirection/redirectionSet1.feature b/packages/app/tests/e2e/features/redirection/redirectionSet1.feature
index 5f4bb8eafb..468f71feab 100644
--- a/packages/app/tests/e2e/features/redirection/redirectionSet1.feature
+++ b/packages/app/tests/e2e/features/redirection/redirectionSet1.feature
@@ -13,7 +13,7 @@ Feature: Redirection
Examples:
| Extra button name | url |
- | Docs | https://docs.zksync.io/build/ |
+ | Docs | https://docs.zksync.io/build/tooling/block-explorer/getting-started.html |
| Terms | https://zksync.io/terms |
| Contact | https://zksync.io/contact |
@@ -32,7 +32,7 @@ Feature: Redirection
@id251
Scenario: Verify redirection for Documentation link
Given I click by text "Documentation"
- Then New page have "https://docs.zksync.io/build/" address
+ Then New page have "https://docs.zksync.io/build/tooling/block-explorer/getting-started.html" address
@id252
Scenario Outline: Verify redirection for "" in BE menu
@@ -45,7 +45,7 @@ Feature: Redirection
| Blocks | /blocks/ |
| Transactions | /transactions/ |
- @id253:II
+ @id253:I
Scenario Outline: Verify redirection for "" in Tools menu
Given I click by text "Tools "
When I click by element with partial href "" and text ""
@@ -56,47 +56,25 @@ Feature: Redirection
| Smart Contract Verification | /contracts/verify |
# | zkEVM Debugger | /tools/debugger |
- @id253:III @featureEnv @testnet
- Scenario Outline: Verify redirection for "" in Tools menu
- Given I click by text "Tools "
- When I click by element with partial href "" and text ""
- Then New page have "" address
-
- Examples:
- | Sub-Section | url |
- | Portal | https://goerli.staging-portal.zksync.dev/ |
-
-
- @id253:IIII @productionEnv @testnet
- Scenario Outline: Verify redirection for "" in Tools menu
+ @id253:III @featureEnv @testnetSmokeSuite @testnet
+ Scenario Outline: Verify redirection for "" in Tools menu (Sepolia)
Given I click by text "Tools "
When I click by element with partial href "" and text ""
Then New page have "" address
Examples:
- | Sub-Section | url | redirect_url |
- | Portal | https://zksync.io/explore#bridges | https://goerli.portal.zksync.io |
+ | Sub-Section | url | redirect_url |
+ | Bridge | https://portal.zksync.io/bridge/?network=sepolia | https://portal.zksync.io/bridge/?network=sepolia |
@id253:IV @featureEnv @mainnet
Scenario Outline: Verify redirection for "" in Tools menu
Given I click by text "Tools "
- When I click by element with partial href "" and text ""
- Then New page have "" address
-
- Examples:
- | Sub-Section | url | redirect_url |
- | Portal | https://staging-portal.zksync.dev/bridge/ | https://staging-portal.zksync.dev |
-
-
- @id253:IV @productionEnv @mainnet
- Scenario Outline: Verify redirection for "" in Tools menu
- Given I click by text "Tools "
- When I click by element with partial href "" and text ""
+ When I click by element with partial href "" and text ""
Then New page have "" address
Examples:
- | Sub-Section | url | redirect_url |
- | Portal | https://zksync.io/explore#bridges | https://portal.zksync.io |
+ | Sub-Section | url |
+ | Bridge | https://portal.zksync.io/bridge/?network=mainnet |
#Account page
@id259 @testnet
diff --git a/packages/app/tests/e2e/src/steps/blockexplorer.steps.ts b/packages/app/tests/e2e/src/steps/blockexplorer.steps.ts
index 507e977eab..6a6651aeb0 100644
--- a/packages/app/tests/e2e/src/steps/blockexplorer.steps.ts
+++ b/packages/app/tests/e2e/src/steps/blockexplorer.steps.ts
@@ -377,6 +377,13 @@ Then("Clipboard contains {string} value", async function (this: ICustomWorld, te
await expect(result).toBe(text);
});
+Then("Clipboard includes {string} value", async function (this: ICustomWorld, text: string) {
+ helper = new Helper(this);
+ result = await helper.getClipboardValue();
+
+ await expect(result.includes(text)).toBe(true);
+});
+
Then("Clipboard value is not empty", async function (this: ICustomWorld) {
helper = new Helper(this);
result = await helper.getClipboardValue();
diff --git a/packages/app/tests/e2e/testId.json b/packages/app/tests/e2e/testId.json
index 071e946243..1eacc3774b 100644
--- a/packages/app/tests/e2e/testId.json
+++ b/packages/app/tests/e2e/testId.json
@@ -1,36 +1,35 @@
{
- "badge": "badge",
- "statusBadge": "status-badge",
- "blocksNumber": "blocks-number",
- "blocksTable": "blocks-table",
- "byteCodeDropDown": "bytecode-dropdown",
- "contractsAddress": "contracts-address",
- "contractVerificationButton": "contract-verification-button",
- "direction": "direction",
- "initiatorsAddress": "initiators-address",
- "fromAddress": "from-address",
- "optimizationRadioButtons": "radio-buttons",
- "pageTitle": "page-title",
- "previousInstructionButton": "previous-instruction-navigation-button",
- "showInstructionMetadataButton": "show-instruction-metadata-button",
- "nextInstructionButton": "next-instruction-navigation-button",
- "traceSearchInput": "trace-search-input",
- "tokensIcon": "tokens-icon",
- "tokenAddress":"tokenAddress",
- "tokenAmount":"token-amount",
- "tokenAmountPrice":"token-amount-price",
- "transactionsHash": "transactions-hash",
- "transactionsMethodName": "transactions-method-name",
- "timestamp": "timestamp",
- "transactionsTable": "transactions-table",
- "latestTransactionsTable": "latest-transaction-table",
- "latestBatchesTable": "latest-batches-table",
- "tokensTable": "tokens-table",
- "toAddress": "to-address",
- "transferType": "transfer-type",
- "transferFromOrigin": "transfer-from-origin",
- "transferToOrigin": "transfer-to-origin",
- "transferFromOriginTablet": "transfer-from-origin-tablet",
- "transferToOriginTablet": "transfer-to-origin-tablet"
- }
-
\ No newline at end of file
+ "badge": "badge",
+ "statusBadge": "status-badge",
+ "blocksNumber": "blocks-number",
+ "blocksTable": "blocks-table",
+ "byteCodeDropDown": "bytecode-dropdown",
+ "contractsAddress": "contracts-address",
+ "contractVerificationButton": "contract-verification-button",
+ "direction": "direction",
+ "initiatorsAddress": "initiators-address",
+ "fromAddress": "from-address",
+ "optimizationRadioButtons": "radio-buttons",
+ "pageTitle": "page-title",
+ "previousInstructionButton": "previous-instruction-navigation-button",
+ "showInstructionMetadataButton": "show-instruction-metadata-button",
+ "nextInstructionButton": "next-instruction-navigation-button",
+ "traceSearchInput": "trace-search-input",
+ "tokensIcon": "tokens-icon",
+ "tokenAddress":"tokenAddress",
+ "tokenAmount":"token-amount",
+ "tokenAmountPrice":"token-amount-price",
+ "transactionsHash": "transactions-hash",
+ "transactionsMethodName": "transactions-method-name",
+ "timestamp": "timestamp",
+ "transactionsTable": "transactions-table",
+ "latestTransactionsTable": "latest-transaction-table",
+ "latestBatchesTable": "latest-batches-table",
+ "tokensTable": "tokens-table",
+ "toAddress": "to-address",
+ "transferType": "transfer-type",
+ "transferFromOrigin": "transfer-from-origin",
+ "transferToOrigin": "transfer-to-origin",
+ "transferFromOriginTablet": "transfer-from-origin-tablet",
+ "transferToOriginTablet": "transfer-to-origin-tablet"
+}
diff --git a/packages/app/tests/mocks.ts b/packages/app/tests/mocks.ts
index 33dd3858dd..2e92981dba 100644
--- a/packages/app/tests/mocks.ts
+++ b/packages/app/tests/mocks.ts
@@ -42,7 +42,6 @@ export const GOERLI_NETWORK: NetworkConfig = {
l2ChainId: 280,
rpcUrl: "",
l2NetworkName: "Goerli",
- l2WalletUrl: "",
l1ExplorerUrl: "http://goerli-block-explorer",
maintenance: false,
published: true,
@@ -56,7 +55,6 @@ export const GOERLI_BETA_NETWORK: NetworkConfig = {
l2ChainId: 270,
rpcUrl: "",
l2NetworkName: "Goerli Beta",
- l2WalletUrl: "",
l1ExplorerUrl: "http://goerli-beta-block-explorer",
maintenance: false,
published: true,
diff --git a/packages/app/tests/views/ContractVerificationView.spec.ts b/packages/app/tests/views/ContractVerificationView.spec.ts
index 8f96244a2b..064dcb4a89 100644
--- a/packages/app/tests/views/ContractVerificationView.spec.ts
+++ b/packages/app/tests/views/ContractVerificationView.spec.ts
@@ -44,13 +44,11 @@ describe("ContractVerificationView:", () => {
en: enUS,
},
});
-
it("has correct title", async () => {
expect(i18n.global.t(routes.find((e) => e.name === "contract-verification")?.meta?.title as string)).toBe(
"Smart Contract Verification"
);
});
-
it("uses contract address from query", async () => {
const wrapper = mount(ContractVerificationView, {
global: {
@@ -113,6 +111,52 @@ describe("ContractVerificationView:", () => {
expect(wrapper.find("#sourceCode").exists()).toBe(false);
expect(wrapper.find(".multi-file-verification").exists()).toBe(true);
});
+ it("shows zkVM checkbox by default", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(true);
+ });
+ it("shows zkVM checkbox when solidity MFV was selected", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ await wrapper.find("#compilerType").trigger("click");
+ await wrapper.find(`[aria-labelledby="compilerType"] > li:nth-child(2)`).trigger("click");
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(true);
+ });
+ it("doesn't show zkVM checkbox when vyper single file verification was select", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ await wrapper.find("#compilerType").trigger("click");
+ await wrapper.find(`[aria-labelledby="compilerType"] > li:nth-child(3)`).trigger("click");
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(false);
+ });
+ it("doesn't show zkVM checkbox when vyper MFV was select", async () => {
+ const wrapper = mount(ContractVerificationView, {
+ global: {
+ stubs: ["router-link"],
+ plugins: [i18n, $testId],
+ },
+ });
+
+ await wrapper.find("#compilerType").trigger("click");
+ await wrapper.find(`[aria-labelledby="compilerType"] > li:nth-child(4)`).trigger("click");
+ expect(wrapper.find(".checkbox-input-container").exists()).toBe(false);
+ });
it("shows custom error text", async () => {
const mock = vi.spyOn(useContractVerification, "default").mockReturnValue({
...useContractVerification.default(),
@@ -309,7 +353,7 @@ describe("ContractVerificationView:", () => {
expect(wrapper.find(".docs-link").text()).toEqual("Details");
expect(wrapper.find(".docs-link").attributes("href")).toEqual(
- "https://era.zksync.io/docs/tools/block-explorer/contract-verification.html#enter-contract-details"
+ "https://docs.zksync.io/build/tooling/block-explorer/contract-verification.html#user-interface"
);
});
it("resets uploaded files block when clicking on clear button", async () => {
diff --git a/packages/data-fetcher/.env.example b/packages/data-fetcher/.env.example
new file mode 100644
index 0000000000..e8fff235c8
--- /dev/null
+++ b/packages/data-fetcher/.env.example
@@ -0,0 +1,13 @@
+LOG_LEVEL=debug
+PORT=3040
+
+BLOCKCHAIN_RPC_URL=http://localhost:3050
+
+RPC_CALLS_DEFAULT_RETRY_TIMEOUT=30000
+RPC_CALLS_QUICK_RETRY_TIMEOUT=500
+RPC_CALLS_RETRIES_MAX_TOTAL_TIMEOUT=90000
+
+RPC_CALLS_CONNECTION_TIMEOUT=20000
+RPC_CALLS_CONNECTION_QUICK_TIMEOUT=10000
+
+MAX_BLOCKS_BATCH_SIZE=20
\ No newline at end of file
diff --git a/packages/data-fetcher/.eslintignore b/packages/data-fetcher/.eslintignore
new file mode 100644
index 0000000000..8f4ceb1f53
--- /dev/null
+++ b/packages/data-fetcher/.eslintignore
@@ -0,0 +1 @@
+/test/scripts/*
\ No newline at end of file
diff --git a/packages/data-fetcher/.eslintrc.js b/packages/data-fetcher/.eslintrc.js
new file mode 100644
index 0000000000..151d5b63d3
--- /dev/null
+++ b/packages/data-fetcher/.eslintrc.js
@@ -0,0 +1,25 @@
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir : __dirname,
+ sourceType: 'module',
+ },
+ plugins: ['@typescript-eslint/eslint-plugin'],
+ extends: [
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:prettier/recommended',
+ ],
+ root: true,
+ env: {
+ node: true,
+ jest: true,
+ },
+ ignorePatterns: ['.eslintrc.js', 'migrationScripts'],
+ rules: {
+ '@typescript-eslint/interface-name-prefix': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ },
+};
diff --git a/packages/data-fetcher/Dockerfile b/packages/data-fetcher/Dockerfile
new file mode 100644
index 0000000000..135a84c9c2
--- /dev/null
+++ b/packages/data-fetcher/Dockerfile
@@ -0,0 +1,43 @@
+FROM node:18.17.1-alpine AS base-stage
+ENV NODE_ENV=production
+
+WORKDIR /usr/src/app
+
+RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
+
+COPY --chown=node:node .npmrc .npmrc
+COPY --chown=node:node lerna.json ./
+COPY --chown=node:node package*.json ./
+COPY --chown=node:node ./packages/data-fetcher/package*.json ./packages/data-fetcher/
+RUN npm ci --ignore-scripts --only=production && npm cache clean --force
+COPY --chown=node:node ./packages/data-fetcher/. ./packages/data-fetcher
+RUN rm -f .npmrc
+
+FROM base-stage AS development-stage
+ENV NODE_ENV=development
+COPY --chown=node:node .npmrc .npmrc
+RUN npm ci
+RUN rm -f .npmrc
+
+FROM development-stage AS build-stage
+RUN npm run build
+
+FROM base-stage AS production-stage
+
+# HEALTHCHECK --interval=30s --timeout=3s --retries=5 \
+# CMD curl -f http://localhost:${PORT}/health || exit 1
+
+COPY --chown=node:node --from=build-stage /usr/src/app/packages/data-fetcher/dist ./packages/data-fetcher/dist
+
+ARG NODE_ENV=production
+ENV NODE_ENV $NODE_ENV
+
+ARG PORT=3040
+ENV PORT $PORT
+
+EXPOSE $PORT 9229 9230
+
+USER node
+WORKDIR /usr/src/app/packages/data-fetcher
+
+CMD [ "node", "dist/main.js" ]
diff --git a/packages/data-fetcher/README.md b/packages/data-fetcher/README.md
new file mode 100644
index 0000000000..e26f2b4993
--- /dev/null
+++ b/packages/data-fetcher/README.md
@@ -0,0 +1,52 @@
+# zkSync Era Block Explorer Data Fetcher
+## Overview
+
+`zkSync Era Block Explorer Data Fetcher` service exposes and implements an HTTP endpoint to retrieve aggregated data for a certain block / range of blocks from the blockchain. This endpoint is called by the [Block Explorer Worker](/packages/worker) service.
+
+## Installation
+
+```bash
+$ npm install
+```
+
+## Setting up env variables
+
+- Create `.env` file in the `data-fetcher` package folder and copy paste `.env.example` content in there.
+```
+cp .env.example .env
+```
+- In order to tell the service where to get the blockchain data from set the value of the `BLOCKCHAIN_RPC_URL` env var to your blockchain RPC API URL. For zkSync Era testnet it can be set to `https://zksync2-testnet.zksync.dev`. For zkSync Era mainnet - `https://zksync2-mainnet.zksync.io`.
+
+## Running the app
+
+```bash
+# development
+$ npm run dev
+
+# watch mode
+$ npm run dev:watch
+
+# debug mode
+$ npm run dev:debug
+
+# production mode
+$ npm run start
+```
+
+## Test
+
+```bash
+# unit tests
+$ npm run test
+
+# unit tests debug mode
+$ npm run test:debug
+
+# test coverage
+$ npm run test:cov
+```
+
+## Development
+
+### Linter
+Run `npm run lint` to make sure the code base follows configured linter rules.
diff --git a/packages/data-fetcher/nest-cli.json b/packages/data-fetcher/nest-cli.json
new file mode 100644
index 0000000000..256648114a
--- /dev/null
+++ b/packages/data-fetcher/nest-cli.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/nest-cli",
+ "collection": "@nestjs/schematics",
+ "sourceRoot": "src"
+}
diff --git a/packages/data-fetcher/package.json b/packages/data-fetcher/package.json
new file mode 100644
index 0000000000..d044a3e55a
--- /dev/null
+++ b/packages/data-fetcher/package.json
@@ -0,0 +1,113 @@
+{
+ "name": "data-fetcher",
+ "version": "0.0.0",
+ "title": "zkSync Era Block Explorer Data Fetcher",
+ "description": "zkSync Era Block Explorer Data Fetcher",
+ "author": "Matter Labs",
+ "private": true,
+ "license": "MIT",
+ "repository": "https://github.com/matter-labs/block-explorer",
+ "scripts": {
+ "prebuild": "rimraf dist",
+ "build": "nest build",
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
+ "dev": "nest start",
+ "dev:watch": "nest start --watch",
+ "dev:debug": "nest start --debug 0.0.0.0:9229 --watch",
+ "start": "node dist/main",
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "test:cov": "jest --coverage",
+ "test:ci": "jest --coverage",
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
+ "test:e2e": "jest --config ./test/jest-e2e.json"
+ },
+ "dependencies": {
+ "@nestjs/common": "^9.0.0",
+ "@nestjs/config": "^2.2.0",
+ "@nestjs/core": "^9.0.0",
+ "@nestjs/platform-express": "^9.0.0",
+ "@nestjs/terminus": "^9.1.2",
+ "@willsoto/nestjs-prometheus": "^4.7.0",
+ "ethers": "^5.7.1",
+ "nest-winston": "^1.7.0",
+ "prom-client": "^14.1.0",
+ "reflect-metadata": "^0.1.13",
+ "rimraf": "^3.0.2",
+ "rxjs": "^7.2.0",
+ "winston": "^3.8.2",
+ "zksync-web3": "0.15.4"
+ },
+ "devDependencies": {
+ "@nestjs/cli": "^9.0.0",
+ "@nestjs/schematics": "^9.0.0",
+ "@nestjs/testing": "^9.0.0",
+ "@types/express": "^4.17.13",
+ "@types/jest": "28.1.8",
+ "@types/supertest": "^2.0.11",
+ "@typescript-eslint/eslint-plugin": "^5.0.0",
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint-config-prettier": "^8.3.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "jest": "29.2.1",
+ "jest-junit": "^14.0.1",
+ "jest-mock-extended": "^3.0.1",
+ "lint-staged": "^13.0.3",
+ "source-map-support": "^0.5.20",
+ "supertest": "^6.1.3",
+ "ts-jest": "29.0.3",
+ "ts-loader": "^9.2.3",
+ "ts-node": "^10.0.0",
+ "tsconfig-paths": "4.1.0"
+ },
+ "jest": {
+ "moduleFileExtensions": [
+ "js",
+ "json",
+ "ts"
+ ],
+ "rootDir": "src",
+ "testRegex": ".*\\.spec\\.ts$",
+ "transform": {
+ "^.+\\.(t|j)s$": "ts-jest"
+ },
+ "collectCoverageFrom": [
+ "**/*.(t|j)s"
+ ],
+ "coverageDirectory": "../coverage",
+ "coverageThreshold": {
+ "global": {
+ "branches": 95,
+ "functions": 84,
+ "lines": 90,
+ "statements": 90
+ }
+ },
+ "testEnvironment": "node",
+ "coveragePathIgnorePatterns": [
+ "src/main.ts",
+ ".module.ts",
+ "src/logger.ts"
+ ],
+ "reporters": [
+ "default",
+ [
+ "jest-junit",
+ {
+ "suiteName": "Data Fetcher Unit tests"
+ }
+ ]
+ ]
+ },
+ "prettier": "@matterlabs/prettier-config",
+ "lint-staged": {
+ "*.{js,ts}": [
+ "npm run lint"
+ ]
+ },
+ "engines": {
+ "npm": ">=9.0.0",
+ "node": ">=18.0.0"
+ }
+}
diff --git a/packages/data-fetcher/src/abis/erc721.json b/packages/data-fetcher/src/abis/erc721.json
new file mode 100644
index 0000000000..b234eedfb6
--- /dev/null
+++ b/packages/data-fetcher/src/abis/erc721.json
@@ -0,0 +1,333 @@
+[
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "mint",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bytes",
+ "name": "_data",
+ "type": "bytes"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "payable": false,
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "ApprovalForAll",
+ "type": "event"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ }
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4"
+ }
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "stateMutability": "view",
+ "type": "function"
+ }
+]
\ No newline at end of file
diff --git a/packages/data-fetcher/src/abis/l2StandardERC20.json b/packages/data-fetcher/src/abis/l2StandardERC20.json
new file mode 100644
index 0000000000..901717080d
--- /dev/null
+++ b/packages/data-fetcher/src/abis/l2StandardERC20.json
@@ -0,0 +1,64 @@
+[
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "l1Token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint8",
+ "name": "decimals",
+ "type": "uint8"
+ }
+ ],
+ "name": "BridgeInitialize",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "l1Token",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "string",
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint8",
+ "name": "decimals",
+ "type": "uint8"
+ }
+ ],
+ "name": "BridgeInitialization",
+ "type": "event"
+ }
+]
\ No newline at end of file
diff --git a/packages/worker/src/abis/transferEventWithNoIndexes.json b/packages/data-fetcher/src/abis/transferEventWithNoIndexes.json
similarity index 100%
rename from packages/worker/src/abis/transferEventWithNoIndexes.json
rename to packages/data-fetcher/src/abis/transferEventWithNoIndexes.json
diff --git a/packages/worker/src/address/address.service.spec.ts b/packages/data-fetcher/src/address/address.service.spec.ts
similarity index 60%
rename from packages/worker/src/address/address.service.spec.ts
rename to packages/data-fetcher/src/address/address.service.spec.ts
index 9352c9957f..d71ba0e796 100644
--- a/packages/worker/src/address/address.service.spec.ts
+++ b/packages/data-fetcher/src/address/address.service.spec.ts
@@ -2,18 +2,15 @@ import { Test } from "@nestjs/testing";
import { Logger } from "@nestjs/common";
import { mock } from "jest-mock-extended";
import { types } from "zksync-web3";
-import { AddressRepository } from "../repositories";
import { AddressService } from "./address.service";
import { BlockchainService } from "../blockchain/blockchain.service";
describe("AddressService", () => {
let blockchainServiceMock: BlockchainService;
- let addressRepositoryMock: AddressRepository;
let addressService: AddressService;
beforeEach(async () => {
blockchainServiceMock = mock();
- addressRepositoryMock = mock();
const app = await Test.createTestingModule({
providers: [
@@ -22,10 +19,6 @@ describe("AddressService", () => {
provide: BlockchainService,
useValue: blockchainServiceMock,
},
- {
- provide: AddressRepository,
- useValue: addressRepositoryMock,
- },
],
}).compile();
@@ -34,7 +27,7 @@ describe("AddressService", () => {
addressService = app.get(AddressService);
});
- describe("saveContractAddresses", () => {
+ describe("getContractAddresses", () => {
const logs = [
mock({
topics: [
@@ -75,55 +68,36 @@ describe("AddressService", () => {
});
it("gets byte code for deployed contracts", async () => {
- await addressService.saveContractAddresses(logs, transactionReceipt);
+ await addressService.getContractAddresses(logs, transactionReceipt);
expect(blockchainServiceMock.getCode).toHaveBeenCalledTimes(2);
expect(blockchainServiceMock.getCode).toHaveBeenCalledWith("0xdc187378edD8Ed1585fb47549Cc5fe633295d571");
expect(blockchainServiceMock.getCode).toHaveBeenCalledWith("0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58");
});
- it("upserts contract addresses", async () => {
- await addressService.saveContractAddresses(logs, transactionReceipt);
- expect(addressRepositoryMock.upsert).toHaveBeenCalledTimes(2);
- expect(addressRepositoryMock.upsert).toHaveBeenCalledWith({
- address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
- bytecode: "bytecode1",
- createdInBlockNumber: transactionReceipt.blockNumber,
- creatorTxHash: transactionReceipt.transactionHash,
- creatorAddress: transactionReceipt.from,
- createdInLogIndex: logs[0].logIndex,
- });
- expect(addressRepositoryMock.upsert).toHaveBeenCalledWith({
- address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58",
- bytecode: "bytecode2",
- createdInBlockNumber: transactionReceipt.blockNumber,
- creatorTxHash: transactionReceipt.transactionHash,
- creatorAddress: transactionReceipt.from,
- createdInLogIndex: logs[2].logIndex,
- });
- });
-
- it("returns created contract addresses", async () => {
- const result = await addressService.saveContractAddresses(logs, transactionReceipt);
- expect(result).toStrictEqual([
+ it("returns contract addresses", async () => {
+ const contractAddresses = await addressService.getContractAddresses(logs, transactionReceipt);
+ expect(contractAddresses).toStrictEqual([
{
address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
- blockNumber: 10,
- creatorAddress: "from",
- transactionHash: "transactionHash",
- logIndex: 1,
+ bytecode: "bytecode1",
+ blockNumber: transactionReceipt.blockNumber,
+ transactionHash: transactionReceipt.transactionHash,
+ creatorAddress: transactionReceipt.from,
+ logIndex: logs[0].logIndex,
},
{
address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58",
- blockNumber: 10,
- creatorAddress: "from",
- transactionHash: "transactionHash",
- logIndex: 3,
+ bytecode: "bytecode2",
+ blockNumber: transactionReceipt.blockNumber,
+ transactionHash: transactionReceipt.transactionHash,
+ creatorAddress: transactionReceipt.from,
+ logIndex: logs[2].logIndex,
},
]);
});
it("returns an empty array if no logs specified", async () => {
- const result = await addressService.saveContractAddresses(null, transactionReceipt);
+ const result = await addressService.getContractAddresses(null, transactionReceipt);
expect(result).toStrictEqual([]);
});
});
diff --git a/packages/worker/src/address/address.service.ts b/packages/data-fetcher/src/address/address.service.ts
similarity index 59%
rename from packages/worker/src/address/address.service.ts
rename to packages/data-fetcher/src/address/address.service.ts
index a9d8d56b83..87b7705966 100644
--- a/packages/worker/src/address/address.service.ts
+++ b/packages/data-fetcher/src/address/address.service.ts
@@ -2,7 +2,6 @@ import { Injectable, Logger } from "@nestjs/common";
import { BigNumber } from "ethers";
import { types } from "zksync-web3";
import { BlockchainService } from "../blockchain/blockchain.service";
-import { AddressRepository } from "../repositories";
import { LogType } from "../log/logType";
import { ExtractContractAddressHandler } from "./interface/extractContractAddressHandler.interface";
import { defaultContractDeployedHandler } from "./extractContractDeployedHandlers";
@@ -17,29 +16,11 @@ export class AddressService {
private readonly logger: Logger;
public changedBalances: Map>;
- constructor(
- private readonly blockchainService: BlockchainService,
- private readonly addressRepository: AddressRepository
- ) {
+ constructor(private readonly blockchainService: BlockchainService) {
this.logger = new Logger(AddressService.name);
}
- private async saveContractAddress(contractAddress: ContractAddress): Promise {
- const bytecode = await this.blockchainService.getCode(contractAddress.address);
-
- const addressDto = {
- address: contractAddress.address,
- bytecode,
- createdInBlockNumber: contractAddress.blockNumber,
- creatorTxHash: contractAddress.transactionHash,
- creatorAddress: contractAddress.creatorAddress,
- createdInLogIndex: contractAddress.logIndex,
- };
-
- await this.addressRepository.upsert(addressDto);
- }
-
- public async saveContractAddresses(
+ public async getContractAddresses(
logs: types.Log[],
transactionReceipt: types.TransactionReceipt
): Promise {
@@ -61,8 +42,15 @@ export class AddressService {
}
}
- this.logger.debug({ message: "Saving contract addresses.", transactionReceipt: transactionReceipt.blockNumber });
- await Promise.all(contractAddresses.map((contractAddress) => this.saveContractAddress(contractAddress)));
+ this.logger.debug({
+ message: "Requesting contracts' bytecode",
+ transactionReceipt: transactionReceipt.blockNumber,
+ });
+ await Promise.all(
+ contractAddresses.map(async (contractAddress) => {
+ contractAddress.bytecode = await this.blockchainService.getCode(contractAddress.address);
+ })
+ );
return contractAddresses;
}
diff --git a/packages/worker/src/address/extractContractDeployedHandlers/default.handler.spec.ts b/packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.spec.ts
similarity index 100%
rename from packages/worker/src/address/extractContractDeployedHandlers/default.handler.spec.ts
rename to packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.spec.ts
diff --git a/packages/worker/src/address/extractContractDeployedHandlers/default.handler.ts b/packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.ts
similarity index 100%
rename from packages/worker/src/address/extractContractDeployedHandlers/default.handler.ts
rename to packages/data-fetcher/src/address/extractContractDeployedHandlers/default.handler.ts
diff --git a/packages/worker/src/address/extractContractDeployedHandlers/index.ts b/packages/data-fetcher/src/address/extractContractDeployedHandlers/index.ts
similarity index 100%
rename from packages/worker/src/address/extractContractDeployedHandlers/index.ts
rename to packages/data-fetcher/src/address/extractContractDeployedHandlers/index.ts
diff --git a/packages/worker/src/address/interface/contractAddress.interface.ts b/packages/data-fetcher/src/address/interface/contractAddress.interface.ts
similarity index 87%
rename from packages/worker/src/address/interface/contractAddress.interface.ts
rename to packages/data-fetcher/src/address/interface/contractAddress.interface.ts
index 9e89041b22..2de3d8deb6 100644
--- a/packages/worker/src/address/interface/contractAddress.interface.ts
+++ b/packages/data-fetcher/src/address/interface/contractAddress.interface.ts
@@ -4,4 +4,5 @@ export interface ContractAddress {
transactionHash: string;
creatorAddress: string;
logIndex: number;
+ bytecode?: string;
}
diff --git a/packages/worker/src/address/interface/extractContractAddressHandler.interface.ts b/packages/data-fetcher/src/address/interface/extractContractAddressHandler.interface.ts
similarity index 100%
rename from packages/worker/src/address/interface/extractContractAddressHandler.interface.ts
rename to packages/data-fetcher/src/address/interface/extractContractAddressHandler.interface.ts
diff --git a/packages/data-fetcher/src/app.module.ts b/packages/data-fetcher/src/app.module.ts
new file mode 100644
index 0000000000..0b4f1f0eff
--- /dev/null
+++ b/packages/data-fetcher/src/app.module.ts
@@ -0,0 +1,38 @@
+import { Module, Logger } from "@nestjs/common";
+import { ConfigModule } from "@nestjs/config";
+import { PrometheusModule } from "@willsoto/nestjs-prometheus";
+import config from "./config";
+import { HealthModule } from "./health/health.module";
+import { BlockchainService } from "./blockchain";
+import { BlockService, BlockController } from "./block";
+import { TransactionService } from "./transaction";
+import { LogService } from "./log";
+import { AddressService } from "./address/address.service";
+import { BalanceService } from "./balance";
+import { TransferService } from "./transfer/transfer.service";
+import { TokenService } from "./token/token.service";
+import { JsonRpcProviderModule } from "./rpcProvider/jsonRpcProvider.module";
+import { MetricsModule } from "./metrics";
+
+@Module({
+ imports: [
+ ConfigModule.forRoot({ isGlobal: true, load: [config] }),
+ PrometheusModule.register(),
+ JsonRpcProviderModule.forRoot(),
+ MetricsModule,
+ HealthModule,
+ ],
+ controllers: [BlockController],
+ providers: [
+ BlockchainService,
+ AddressService,
+ BalanceService,
+ TransferService,
+ TokenService,
+ TransactionService,
+ LogService,
+ BlockService,
+ Logger,
+ ],
+})
+export class AppModule {}
diff --git a/packages/data-fetcher/src/balance/balance.service.spec.ts b/packages/data-fetcher/src/balance/balance.service.spec.ts
new file mode 100644
index 0000000000..4a2b2baed2
--- /dev/null
+++ b/packages/data-fetcher/src/balance/balance.service.spec.ts
@@ -0,0 +1,534 @@
+import { Test, TestingModuleBuilder } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { mock } from "jest-mock-extended";
+import { BigNumber } from "ethers";
+import { utils } from "zksync-web3";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TokenType } from "../token/token.service";
+import { BalanceService } from "./";
+
+describe("BalanceService", () => {
+ let testingModuleBuilder: TestingModuleBuilder;
+ let blockchainServiceMock: BlockchainService;
+ let balanceService: BalanceService;
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock();
+
+ testingModuleBuilder = Test.createTestingModule({
+ providers: [
+ BalanceService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ ],
+ });
+ const app = await testingModuleBuilder.compile();
+
+ app.useLogger(mock());
+
+ balanceService = app.get(BalanceService);
+ });
+
+ describe("clearTrackedState", () => {
+ const blockNumber = 10;
+ const blockNumber2 = 15;
+
+ beforeEach(() => {
+ balanceService.changedBalances.set(
+ blockNumber,
+ new Map>()
+ );
+ balanceService.changedBalances.set(
+ blockNumber2,
+ new Map>()
+ );
+ });
+
+ it("clears tracked balances for the specified block number", () => {
+ balanceService.clearTrackedState(blockNumber);
+ expect(balanceService.changedBalances.size).toBe(1);
+ expect(balanceService.changedBalances.has(blockNumber2)).toBe(true);
+ });
+ });
+
+ describe("trackChangedBalances", () => {
+ const transfers = [
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.ETH,
+ }),
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.ETH,
+ }),
+ mock({
+ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
+ from: "0x0000000000000000000000000000000000000000",
+ to: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
+ blockNumber: 10,
+ tokenType: TokenType.ERC20,
+ }),
+ ];
+
+ it("processes null as a transfers array", () => {
+ balanceService.trackChangedBalances(null);
+ expect(balanceService.changedBalances.size).toBe(0);
+ });
+
+ it("processes empty transfers array", () => {
+ balanceService.trackChangedBalances([]);
+ expect(balanceService.changedBalances.size).toBe(0);
+ });
+
+ it("does not track changed balance for 0x000 address", () => {
+ const transfers = [
+ mock({
+ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
+ from: "0x000000000000000000000000000000000000800a",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.ERC20,
+ }),
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0x000000000000000000000000000000000000800a",
+ to: "0x0000000000000000000000000000000000008001",
+ blockNumber: 10,
+ tokenType: TokenType.ETH,
+ }),
+ mock({
+ tokenAddress: "0x000000000000000000000000000000000000800a",
+ from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
+ to: "0x0000000000000000000000000000000000000000",
+ blockNumber: 10,
+ tokenType: TokenType.ETH,
+ }),
+ mock({
+ tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
+ from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
+ to: "0x0000000000000000000000000000000000000000",
+ blockNumber: 10,
+ tokenType: TokenType.ERC20,
+ }),
+ ];
+
+ balanceService.trackChangedBalances(transfers);
+ expect(balanceService.changedBalances.has(transfers[0].blockNumber)).toBe(true);
+ expect(balanceService.changedBalances.get(transfers[0].blockNumber).size).toBe(3);
+ const blockChangedBalances = balanceService.changedBalances.get(transfers[0].blockNumber);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true);
+ expect(blockChangedBalances.has("0x000000000000000000000000000000000000800a")).toBe(true);
+ expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000000000")).toBe(false);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0x000000000000000000000000000000000000800a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ });
+
+ it("tracks changed balance addresses for transfers", () => {
+ balanceService.trackChangedBalances(transfers);
+ expect(balanceService.changedBalances.has(transfers[0].blockNumber)).toBe(true);
+ const blockChangedBalances = balanceService.changedBalances.get(transfers[0].blockNumber);
+ expect(blockChangedBalances.size).toBe(3);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true);
+ expect(blockChangedBalances.has("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")).toBe(true);
+ expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ });
+
+ it("merge changed balances with existing changed balances for the block", () => {
+ const existingBlockBalances = new Map>();
+ existingBlockBalances.set(
+ "0x0000000000000000000000000000000000008007",
+ new Map([
+ ["0x000000000000000000000000000000000000800a", { balance: undefined, tokenType: TokenType.ETH }],
+ ["0x0000000000000000000000000000000000008123", { balance: undefined, tokenType: TokenType.ERC20 }],
+ ])
+ );
+
+ existingBlockBalances.set(
+ "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
+ new Map([
+ ["0x000000000000000000000000000000000000800a", { balance: undefined, tokenType: TokenType.ETH }],
+ ])
+ );
+
+ balanceService.changedBalances.set(transfers[0].blockNumber, existingBlockBalances);
+
+ balanceService.trackChangedBalances(transfers);
+ expect(balanceService.changedBalances.has(transfers[0].blockNumber)).toBe(true);
+ const blockChangedBalances = balanceService.changedBalances.get(transfers[0].blockNumber);
+ expect(blockChangedBalances.size).toBe(4);
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008007")).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .has("0x0000000000000000000000000000000000008123")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008007")
+ .get("0x0000000000000000000000000000000000008123")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ expect(blockChangedBalances.has("0x0000000000000000000000000000000000008001")).toBe(true);
+ expect(blockChangedBalances.has("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")).toBe(true);
+ expect(blockChangedBalances.has("0xd206eaf6819007535e893410cfa01885ce40e99a")).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x0000000000000000000000000000000000008001")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .has("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0x36615cf349d7f6344891b1e7ca7c72883f5dc049")
+ .get("0x2392e98fb47cf05773144db3ce8002fac4f39c84")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ERC20,
+ });
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .has("0x000000000000000000000000000000000000800a")
+ ).toBe(true);
+ expect(
+ blockChangedBalances
+ .get("0xd206eaf6819007535e893410cfa01885ce40e99a")
+ .get("0x000000000000000000000000000000000000800a")
+ ).toEqual({
+ balance: undefined,
+ tokenType: TokenType.ETH,
+ });
+ });
+ });
+
+ describe("getChangedBalances", () => {
+ const blockNumber = 5;
+ const addresses = ["36615cf349d7f6344891b1e7ca7c72883f5dc049", "0000000000000000000000000000000000008001"];
+
+ const tokenAddresses = [
+ ["0x0000000000000000000000000000000000008001", "0x000000000000000000000000000000000000800a"],
+ ["0x36615cf349d7f6344891b1e7ca7c72883f5dc049", "0x000000000000000000000000000000000000800a"],
+ ];
+
+ beforeEach(() => {
+ const blockBalances = new Map>();
+ blockBalances.set(
+ utils.ETH_ADDRESS,
+ new Map([
+ [utils.ETH_ADDRESS, { balance: undefined, tokenType: TokenType.ETH }],
+ ])
+ );
+ blockBalances.set(
+ addresses[0],
+ new Map([
+ [tokenAddresses[0][0], { balance: undefined, tokenType: TokenType.ERC20 }],
+ [tokenAddresses[0][1], { balance: undefined, tokenType: TokenType.ETH }],
+ ])
+ );
+ blockBalances.set(
+ addresses[1],
+ new Map([
+ [tokenAddresses[1][0], { balance: undefined, tokenType: TokenType.ERC20 }],
+ [tokenAddresses[1][1], { balance: undefined, tokenType: TokenType.ETH }],
+ ])
+ );
+ balanceService.changedBalances.set(blockNumber, blockBalances);
+
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(1));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(2));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(3));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(4));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(5));
+ });
+
+ it("processes block number with no tracked balances", async () => {
+ await balanceService.getChangedBalances(blockNumber + 10);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledTimes(0);
+ });
+
+ it("requests balances from the blockchain service", async () => {
+ await balanceService.getChangedBalances(blockNumber);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledTimes(5);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(utils.ETH_ADDRESS, blockNumber, utils.ETH_ADDRESS);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[0], blockNumber, tokenAddresses[0][0]);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[0], blockNumber, tokenAddresses[0][1]);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[1], blockNumber, tokenAddresses[1][0]);
+ expect(blockchainServiceMock.getBalance).toHaveBeenCalledWith(addresses[1], blockNumber, tokenAddresses[1][1]);
+ });
+
+ it("returns changed balances", async () => {
+ const changedBalances = await balanceService.getChangedBalances(blockNumber);
+ expect(changedBalances).toEqual([
+ {
+ address: "0x0000000000000000000000000000000000000000",
+ blockNumber: 5,
+ tokenAddress: "0x0000000000000000000000000000000000000000",
+ balance: BigNumber.from(1),
+ tokenType: TokenType.ETH,
+ },
+ {
+ address: addresses[0],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[0][0],
+ balance: BigNumber.from(2),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[0],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[0][1],
+ balance: BigNumber.from(3),
+ tokenType: TokenType.ETH,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][0],
+ balance: BigNumber.from(4),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][1],
+ balance: BigNumber.from(5),
+ tokenType: TokenType.ETH,
+ },
+ ]);
+ });
+
+ describe("when some getBalance throw errors", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getBalance").mockReset();
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(1));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(2));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockRejectedValueOnce("balanceOf function is not defined");
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(4));
+ jest.spyOn(blockchainServiceMock, "getBalance").mockResolvedValueOnce(BigNumber.from(5));
+ });
+
+ it("returns only successfully fetched balances", async () => {
+ const changedBalances = await balanceService.getChangedBalances(blockNumber);
+ expect(changedBalances).toEqual([
+ {
+ address: "0x0000000000000000000000000000000000000000",
+ blockNumber: 5,
+ tokenAddress: "0x0000000000000000000000000000000000000000",
+ balance: BigNumber.from(1),
+ tokenType: TokenType.ETH,
+ },
+ {
+ address: addresses[0],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[0][0],
+ balance: BigNumber.from(2),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][0],
+ balance: BigNumber.from(4),
+ tokenType: TokenType.ERC20,
+ },
+ {
+ address: addresses[1],
+ blockNumber: 5,
+ tokenAddress: tokenAddresses[1][1],
+ balance: BigNumber.from(5),
+ tokenType: TokenType.ETH,
+ },
+ ]);
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/balance/balance.service.ts b/packages/data-fetcher/src/balance/balance.service.ts
new file mode 100644
index 0000000000..8876adb6bb
--- /dev/null
+++ b/packages/data-fetcher/src/balance/balance.service.ts
@@ -0,0 +1,122 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { BigNumber } from "ethers";
+import { utils } from "zksync-web3";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TokenType } from "../token/token.service";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+
+export type BlockChangedBalances = Map>;
+
+export interface Balance {
+ address: string;
+ tokenAddress: string;
+ blockNumber: number;
+ balance: BigNumber;
+ tokenType: TokenType;
+}
+
+@Injectable()
+export class BalanceService {
+ private readonly logger: Logger;
+ public changedBalances: Map;
+
+ constructor(private readonly blockchainService: BlockchainService) {
+ this.logger = new Logger(BalanceService.name);
+ this.changedBalances = new Map();
+ }
+
+ public clearTrackedState(blockNumber: number): void {
+ this.changedBalances.delete(blockNumber);
+ }
+
+ public trackChangedBalances(transfers: Transfer[]): void {
+ if (!transfers?.length) {
+ return;
+ }
+
+ const blockChangedBalances =
+ this.changedBalances.get(transfers[0].blockNumber) ||
+ new Map>();
+
+ for (const transfer of transfers) {
+ const changedBalancesAddresses = new Set([transfer.from, transfer.to]);
+ for (const changedBalanceAddress of changedBalancesAddresses) {
+ if (changedBalanceAddress === utils.ETH_ADDRESS) {
+ continue;
+ }
+
+ if (!blockChangedBalances.has(changedBalanceAddress)) {
+ blockChangedBalances.set(
+ changedBalanceAddress,
+ new Map()
+ );
+ }
+
+ blockChangedBalances
+ .get(changedBalanceAddress)
+ .set(transfer.tokenAddress, { balance: undefined, tokenType: transfer.tokenType });
+ }
+ }
+
+ this.changedBalances.set(transfers[0].blockNumber, blockChangedBalances);
+ }
+
+ public async getChangedBalances(blockNumber: number): Promise {
+ if (!this.changedBalances.has(blockNumber)) {
+ return null;
+ }
+
+ const blockChangedBalances = this.changedBalances.get(blockNumber);
+ const balanceAddresses: string[][] = [];
+ const getBalancePromises: Promise[] = [];
+
+ for (const [address, tokenAddresses] of blockChangedBalances) {
+ for (const [tokenAddress] of tokenAddresses) {
+ balanceAddresses.push([address, tokenAddress]);
+ getBalancePromises.push(this.blockchainService.getBalance(address, blockNumber, tokenAddress));
+ }
+ }
+
+ this.logger.debug({ message: "Getting balances from the blockchain.", blockNumber });
+ const balances = await Promise.allSettled(getBalancePromises);
+
+ for (let i = 0; i < balances.length; i++) {
+ const [address, tokenAddress] = balanceAddresses[i];
+ if (balances[i].status === "fulfilled") {
+ const blockChangedBalancesForAddress = blockChangedBalances.get(address);
+ blockChangedBalancesForAddress.set(tokenAddress, {
+ balance: (balances[i] as PromiseFulfilledResult).value,
+ tokenType: blockChangedBalancesForAddress.get(tokenAddress).tokenType,
+ });
+ } else {
+ this.logger.warn({
+ message: "Get balance for token failed",
+ tokenAddress,
+ address,
+ blockNumber,
+ reason: (balances[i] as PromiseRejectedResult).reason,
+ });
+ // since we have internal retries for contract and RPC calls if an error gets through to here
+ // it means it is not retryable and happens because balanceOf function is not defined or fails with a permanent error in the contract
+ // in such case we're not saving the balance at all
+ blockChangedBalances.get(address).delete(tokenAddress);
+ }
+ }
+
+ const balanceRecords: Balance[] = [];
+
+ for (const [address, addressTokenBalances] of blockChangedBalances) {
+ for (const [tokenAddress, addressTokenBalance] of addressTokenBalances) {
+ balanceRecords.push({
+ address,
+ tokenAddress,
+ blockNumber,
+ balance: addressTokenBalance.balance,
+ tokenType: addressTokenBalance.tokenType,
+ });
+ }
+ }
+
+ return balanceRecords;
+ }
+}
diff --git a/packages/data-fetcher/src/balance/index.ts b/packages/data-fetcher/src/balance/index.ts
new file mode 100644
index 0000000000..ea2baee9f5
--- /dev/null
+++ b/packages/data-fetcher/src/balance/index.ts
@@ -0,0 +1 @@
+export * from "./balance.service";
diff --git a/packages/data-fetcher/src/block/block.controller.spec.ts b/packages/data-fetcher/src/block/block.controller.spec.ts
new file mode 100644
index 0000000000..3abe2b5662
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.controller.spec.ts
@@ -0,0 +1,89 @@
+import { Test } from "@nestjs/testing";
+import { mock } from "jest-mock-extended";
+import { ConfigService } from "@nestjs/config";
+import { BadRequestException } from "@nestjs/common";
+import { BlockController } from "./block.controller";
+import { BlockService } from "./block.service";
+
+describe("BlockController", () => {
+ let controller: BlockController;
+ let configServiceMock: ConfigService;
+ let blockServiceMock: BlockService;
+
+ beforeEach(async () => {
+ configServiceMock = mock({
+ get: jest.fn().mockReturnValue(10),
+ });
+ blockServiceMock = mock();
+ const module = await Test.createTestingModule({
+ controllers: [BlockController],
+ providers: [
+ {
+ provide: ConfigService,
+ useValue: configServiceMock,
+ },
+ {
+ provide: BlockService,
+ useValue: blockServiceMock,
+ },
+ ],
+ }).compile();
+ controller = module.get(BlockController);
+ });
+
+ describe("getBlocks", () => {
+ it("throws an error if requested blocks range is larger than allowed", async () => {
+ await expect(controller.getBlocks(0, 11)).rejects.toThrowError(
+ new BadRequestException(`Max supported batch is 10.`)
+ );
+ });
+
+ it("throws an error if to block number is less than from block number", async () => {
+ await expect(controller.getBlocks(1, 0)).rejects.toThrowError(
+ new BadRequestException(`To block is less than from block.`)
+ );
+ });
+
+ it("returns blocks information for requested blocks", async () => {
+ const blockDetails = [
+ {
+ block: { number: 3 },
+ },
+ {
+ block: { number: 4 },
+ },
+ {
+ block: { number: 5 },
+ },
+ ];
+
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[0]);
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[1]);
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[2]);
+
+ const blocksData = await controller.getBlocks(3, 5);
+ expect(blockServiceMock.getData).toHaveBeenCalledTimes(3);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(3);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(4);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(5);
+
+ expect(blocksData).toEqual(blockDetails);
+ });
+
+ it("returns block information if only from block is specified", async () => {
+ const blockDetails = [
+ {
+ block: { number: 3 },
+ },
+ ];
+
+ (blockServiceMock.getData as jest.Mock).mockResolvedValueOnce(blockDetails[0]);
+
+ const blocksData = await controller.getBlocks(3);
+ expect(blockServiceMock.getData).toHaveBeenCalledTimes(1);
+ expect(blockServiceMock.getData).toHaveBeenCalledWith(3);
+
+ expect(blocksData).toEqual(blockDetails);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/block/block.controller.ts b/packages/data-fetcher/src/block/block.controller.ts
new file mode 100644
index 0000000000..333aa84a32
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.controller.ts
@@ -0,0 +1,37 @@
+import { Controller, Get, Query, BadRequestException } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { ParseLimitedIntPipe } from "../common/pipes/parseLimitedInt.pipe";
+import { BlockService, BlockData } from "./";
+
+@Controller("blocks")
+export class BlockController {
+ private readonly maxBlocksBatchSize: number;
+
+ constructor(configService: ConfigService, private readonly blockService: BlockService) {
+ this.maxBlocksBatchSize = configService.get("maxBlocksBatchSize");
+ }
+
+ @Get("")
+ public async getBlocks(
+ @Query("from", new ParseLimitedIntPipe({ min: 0 })) from: number,
+ @Query("to", new ParseLimitedIntPipe({ min: 0, isOptional: true })) to?: number | null
+ ): Promise {
+ to = to != null ? to : from;
+
+ if (to < from) {
+ throw new BadRequestException("To block is less than from block.");
+ }
+
+ // +1 since from and to are inclusive
+ if (to - from + 1 > this.maxBlocksBatchSize) {
+ throw new BadRequestException(`Max supported batch is ${this.maxBlocksBatchSize}.`);
+ }
+
+ const getBlockDataPromises = [];
+ for (let blockNumber = from; blockNumber <= to; blockNumber++) {
+ getBlockDataPromises.push(this.blockService.getData(blockNumber));
+ }
+
+ return await Promise.all(getBlockDataPromises);
+ }
+}
diff --git a/packages/data-fetcher/src/block/block.service.spec.ts b/packages/data-fetcher/src/block/block.service.spec.ts
new file mode 100644
index 0000000000..91f5a507fb
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.service.spec.ts
@@ -0,0 +1,315 @@
+import { Test } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { types } from "zksync-web3";
+import { mock } from "jest-mock-extended";
+import { BigNumber } from "ethers";
+import { TransactionService } from "../transaction";
+import { LogService } from "../log";
+import { BlockchainService } from "../blockchain";
+import { BalanceService } from "../balance";
+import { BlockService } from "./";
+import { TokenType } from "../token/token.service";
+
+describe("BlockService", () => {
+ let blockService: BlockService;
+ let blockchainServiceMock: BlockchainService;
+ let transactionServiceMock: TransactionService;
+ let logServiceMock: LogService;
+ let balanceServiceMock: BalanceService;
+
+ let startGetBlockInfoDurationMetricMock: jest.Mock;
+ let stopGetBlockInfoDurationMetricMock: jest.Mock;
+
+ let startBlockDurationMetricMock: jest.Mock;
+ let stopBlockDurationMetricMock: jest.Mock;
+
+ let startBalancesDurationMetricMock: jest.Mock;
+ let stopBalancesDurationMetricMock: jest.Mock;
+
+ const getBlockService = async () => {
+ const app = await Test.createTestingModule({
+ providers: [
+ BlockService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ {
+ provide: TransactionService,
+ useValue: transactionServiceMock,
+ },
+ {
+ provide: LogService,
+ useValue: logServiceMock,
+ },
+ {
+ provide: BalanceService,
+ useValue: balanceServiceMock,
+ },
+ {
+ provide: "PROM_METRIC_BLOCK_PROCESSING_DURATION_SECONDS",
+ useValue: {
+ startTimer: startBlockDurationMetricMock,
+ },
+ },
+ {
+ provide: "PROM_METRIC_BALANCES_PROCESSING_DURATION_SECONDS",
+ useValue: {
+ startTimer: startBalancesDurationMetricMock,
+ },
+ },
+ {
+ provide: "PROM_METRIC_GET_BLOCK_INFO_DURATION_SECONDS",
+ useValue: {
+ startTimer: startGetBlockInfoDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ return app.get(BlockService);
+ };
+
+ const transactionData = [
+ {
+ transaction: {
+ hash: "transactionHash1",
+ },
+ },
+ {
+ transaction: {
+ hash: "transactionHash2",
+ },
+ },
+ ];
+
+ const blockLogData = {
+ logs: [{ logIndex: 0 }, { logIndex: 1 }],
+ transfers: [{ logIndex: 0 }, { logIndex: 1 }],
+ };
+
+ const blockInfoData = {
+ hash: "hash",
+ transactions: ["transactionHash1", "transactionHash2"],
+ };
+
+ const blockDetailsData = {
+ blockHash: "blockHash",
+ };
+
+ const blockChangedBalances = [
+ {
+ address: "0x0000000000000000000000000000000000000000",
+ blockNumber: 5,
+ tokenAddress: "0x0000000000000000000000000000000000000000",
+ balance: BigNumber.from(1),
+ tokenType: TokenType.ETH,
+ },
+ ];
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock({
+ getBlock: jest.fn().mockResolvedValue(blockInfoData),
+ getBlockDetails: jest.fn().mockResolvedValue(blockDetailsData),
+ getLogs: jest.fn().mockResolvedValue([]),
+ });
+ transactionServiceMock = mock({
+ getData: jest.fn().mockResolvedValueOnce(transactionData[0]).mockResolvedValueOnce(transactionData[1]),
+ });
+ logServiceMock = mock({
+ getData: jest.fn().mockResolvedValue(blockLogData),
+ });
+ balanceServiceMock = mock({
+ getChangedBalances: jest.fn().mockResolvedValueOnce(blockChangedBalances),
+ clearTrackedState: jest.fn(),
+ });
+
+ stopGetBlockInfoDurationMetricMock = jest.fn();
+ startGetBlockInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetBlockInfoDurationMetricMock);
+
+ stopBlockDurationMetricMock = jest.fn();
+ startBlockDurationMetricMock = jest.fn().mockReturnValue(stopBlockDurationMetricMock);
+
+ stopBalancesDurationMetricMock = jest.fn();
+ startBalancesDurationMetricMock = jest.fn().mockReturnValue(stopBalancesDurationMetricMock);
+
+ blockService = await getBlockService();
+ });
+
+ const blockNumber = 1;
+
+ describe("getData", () => {
+ it("starts the get block info metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(startGetBlockInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns data with block and block details info", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockchainServiceMock.getBlock).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getBlock).toHaveBeenCalledWith(blockNumber);
+ expect(blockchainServiceMock.getBlockDetails).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getBlockDetails).toHaveBeenCalledWith(blockNumber);
+ expect(blockData.block).toEqual(blockInfoData);
+ expect(blockData.blockDetails).toEqual(blockDetailsData);
+ });
+
+ it("stops the get block info metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopGetBlockInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("starts the block processing duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(startBlockDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns data with block info", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData.block).toEqual(blockInfoData);
+ expect(blockData.blockDetails).toEqual(blockDetailsData);
+ });
+
+ it("returns null if block does not exist in blockchain", async () => {
+ (blockchainServiceMock.getBlock as jest.Mock).mockResolvedValue(null);
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData).toBeNull();
+ });
+
+ it("returns null if block details does not exist in blockchain", async () => {
+ (blockchainServiceMock.getBlockDetails as jest.Mock).mockResolvedValue(null);
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData).toBeNull();
+ });
+
+ it("returns block transactions data", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(transactionServiceMock.getData).toHaveBeenCalledTimes(2);
+ expect(transactionServiceMock.getData).toHaveBeenCalledWith(blockInfoData.transactions[0], blockDetailsData);
+ expect(transactionServiceMock.getData).toHaveBeenCalledWith(blockInfoData.transactions[1], blockDetailsData);
+ expect(blockData.transactions).toEqual(transactionData);
+ });
+
+ describe("when processing fails with an error", () => {
+ beforeEach(() => {
+ jest.spyOn(transactionServiceMock, "getData").mockReset();
+ jest.spyOn(transactionServiceMock, "getData").mockRejectedValue(new Error("log service error"));
+ });
+
+ it("throws the generated error", async () => {
+ await expect(blockService.getData(blockNumber)).rejects.toThrowError(new Error("log service error"));
+ });
+
+ it("stops block processing duration metric and sets label to error", async () => {
+ expect.assertions(2);
+ try {
+ await blockService.getData(blockNumber);
+ } catch {
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledWith({
+ status: "error",
+ action: "get",
+ });
+ }
+ });
+
+ it("clears tracked address changes state", async () => {
+ expect.assertions(2);
+ try {
+ await blockService.getData(blockNumber);
+ } catch {
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledWith(blockNumber);
+ }
+ });
+ });
+
+ describe("when block does not contain transactions", () => {
+ let logs: types.Log[];
+ beforeEach(() => {
+ const blockData = {
+ ...blockInfoData,
+ transactions: [],
+ } as types.Block;
+ jest.spyOn(blockchainServiceMock, "getBlock").mockReset();
+ jest.spyOn(blockchainServiceMock, "getBlock").mockResolvedValueOnce(blockData);
+ logs = [{ logIndex: 0 } as types.Log, { logIndex: 1 } as types.Log];
+ jest.spyOn(blockchainServiceMock, "getLogs").mockResolvedValueOnce(logs);
+ });
+
+ it("reads logs for block from the blockchain", async () => {
+ await blockService.getData(blockNumber);
+ expect(blockchainServiceMock.getLogs).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getLogs).toHaveBeenCalledWith({
+ fromBlock: blockNumber,
+ toBlock: blockNumber,
+ });
+ });
+
+ it("gets and returns block data", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(logServiceMock.getData).toHaveBeenCalledTimes(1);
+ expect(logServiceMock.getData).toHaveBeenCalledWith(logs, blockDetailsData);
+ expect(blockData.blockLogs).toEqual(blockLogData.logs);
+ expect(blockData.blockTransfers).toEqual(blockLogData.transfers);
+ });
+ });
+
+ it("starts the balances duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(startBalancesDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns changed balances", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledWith(blockNumber);
+ expect(blockData.changedBalances).toEqual(blockChangedBalances);
+ });
+
+ it("returns empty array as changed balances if there are no any", async () => {
+ (balanceServiceMock.getChangedBalances as jest.Mock).mockReset();
+ (balanceServiceMock.getChangedBalances as jest.Mock).mockResolvedValue(null);
+ const blockData = await blockService.getData(blockNumber);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.getChangedBalances).toHaveBeenCalledWith(blockNumber);
+ expect(blockData.changedBalances).toEqual([]);
+ });
+
+ it("stops the balances duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopBalancesDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the duration metric", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("sets duration metric label to success", async () => {
+ await blockService.getData(blockNumber);
+ expect(stopBlockDurationMetricMock).toHaveBeenCalledWith({
+ status: "success",
+ action: "get",
+ });
+ });
+
+ it("clears tracked address changes state", async () => {
+ await blockService.getData(blockNumber);
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.clearTrackedState).toHaveBeenCalledWith(blockNumber);
+ });
+
+ it("returns empty block logs array", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData.blockLogs).toEqual([]);
+ });
+
+ it("returns empty block transfers array", async () => {
+ const blockData = await blockService.getData(blockNumber);
+ expect(blockData.blockTransfers).toEqual([]);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/block/block.service.ts b/packages/data-fetcher/src/block/block.service.ts
new file mode 100644
index 0000000000..5c1bf7eae7
--- /dev/null
+++ b/packages/data-fetcher/src/block/block.service.ts
@@ -0,0 +1,103 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { Histogram } from "prom-client";
+import { types } from "zksync-web3";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { BalanceService, Balance } from "../balance/balance.service";
+import { TransactionService, TransactionData } from "../transaction";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { LogService, LogsData } from "../log";
+import {
+ BLOCK_PROCESSING_DURATION_METRIC_NAME,
+ BALANCES_PROCESSING_DURATION_METRIC_NAME,
+ GET_BLOCK_INFO_DURATION_METRIC_NAME,
+ BlockProcessingMetricLabels,
+ ProcessingActionMetricLabel,
+} from "../metrics";
+
+export interface BlockData {
+ block: types.Block;
+ blockDetails: types.BlockDetails;
+ transactions: TransactionData[];
+ blockLogs: types.Log[];
+ blockTransfers: Transfer[];
+ changedBalances: Balance[];
+}
+
+@Injectable()
+export class BlockService {
+ private readonly logger: Logger;
+
+ public constructor(
+ private readonly blockchainService: BlockchainService,
+ private readonly transactionService: TransactionService,
+ private readonly logService: LogService,
+ private readonly balanceService: BalanceService,
+ @InjectMetric(BLOCK_PROCESSING_DURATION_METRIC_NAME)
+ private readonly processingDurationMetric: Histogram,
+ @InjectMetric(BALANCES_PROCESSING_DURATION_METRIC_NAME)
+ private readonly balancesProcessingDurationMetric: Histogram,
+ @InjectMetric(GET_BLOCK_INFO_DURATION_METRIC_NAME)
+ private readonly getBlockInfoDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(BlockService.name);
+ }
+
+ public async getData(blockNumber: number): Promise {
+ const stopDurationMeasuring = this.processingDurationMetric.startTimer();
+
+ this.logger.debug({ message: "Getting block data from the blockchain", blockNumber });
+ const stopGetBlockInfoDurationMetric = this.getBlockInfoDurationMetric.startTimer();
+ const [block, blockDetails] = await Promise.all([
+ this.blockchainService.getBlock(blockNumber),
+ this.blockchainService.getBlockDetails(blockNumber),
+ ]);
+ stopGetBlockInfoDurationMetric();
+
+ if (!block || !blockDetails) {
+ return null;
+ }
+
+ let blockProcessingStatus = "success";
+ let transactions: TransactionData[] = [];
+ let blockLogData: LogsData;
+ let changedBalances: Balance[];
+ let blockLogs: types.Log[];
+
+ try {
+ transactions = await Promise.all(
+ block.transactions.map((transactionHash) => this.transactionService.getData(transactionHash, blockDetails))
+ );
+
+ if (block.transactions.length === 0) {
+ blockLogs = await this.blockchainService.getLogs({
+ fromBlock: blockNumber,
+ toBlock: blockNumber,
+ });
+
+ blockLogData = await this.logService.getData(blockLogs, blockDetails);
+ }
+
+ const stopBalancesDurationMeasuring = this.balancesProcessingDurationMetric.startTimer();
+ this.logger.debug({ message: "Getting balances", blockNumber });
+ changedBalances = await this.balanceService.getChangedBalances(blockNumber);
+ stopBalancesDurationMeasuring();
+ } catch (error) {
+ blockProcessingStatus = "error";
+ throw error;
+ } finally {
+ this.balanceService.clearTrackedState(blockNumber);
+ stopDurationMeasuring({ status: blockProcessingStatus, action: "get" });
+ }
+
+ this.logger.debug({ message: "Successfully generated block data", blockNumber });
+ return {
+ block,
+ blockDetails,
+ blockLogs: blockLogs || [],
+ blockTransfers: blockLogData?.transfers || [],
+ transactions,
+ changedBalances: changedBalances || [],
+ };
+ }
+}
diff --git a/packages/data-fetcher/src/block/index.ts b/packages/data-fetcher/src/block/index.ts
new file mode 100644
index 0000000000..7230f09c4f
--- /dev/null
+++ b/packages/data-fetcher/src/block/index.ts
@@ -0,0 +1,2 @@
+export * from "./block.service";
+export * from "./block.controller";
diff --git a/packages/data-fetcher/src/blockchain/blockchain.service.spec.ts b/packages/data-fetcher/src/blockchain/blockchain.service.spec.ts
new file mode 100644
index 0000000000..2e2a1f4b12
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/blockchain.service.spec.ts
@@ -0,0 +1,2384 @@
+import * as ethers from "ethers";
+import { mock } from "jest-mock-extended";
+import { utils, types } from "zksync-web3";
+import { Test, TestingModule } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import * as timersPromises from "timers/promises";
+import { BlockchainService, BridgeAddresses } from "./blockchain.service";
+import { JsonRpcProviderBase } from "../rpcProvider";
+import { RetryableContract } from "./retryableContract";
+
+jest.mock("./retryableContract");
+const metricProviderKey = "PROM_METRIC_BLOCKCHAIN_RPC_CALL_DURATION_SECONDS";
+
+describe("BlockchainService", () => {
+ let app: TestingModule;
+ const l2Erc20Bridge = "l2Erc20Bridge";
+ let blockchainService: BlockchainService;
+ let provider: JsonRpcProviderBase;
+ let providerFormatterMock;
+ let configServiceMock: ConfigService;
+ let startRpcCallDurationMetricMock: jest.Mock;
+ let stopRpcCallDurationMetricMock: jest.Mock;
+ const defaultRetryTimeout = 2;
+ const quickRetryTimeout = 1;
+ const retriesMaxTotalTimeout = 100;
+
+ beforeEach(async () => {
+ providerFormatterMock = {
+ blockTag: jest.fn(),
+ };
+
+ provider = mock({
+ formatter: providerFormatterMock,
+ });
+
+ configServiceMock = mock({
+ get: jest.fn().mockImplementation((configName) => {
+ switch (configName) {
+ case "blockchain.rpcCallDefaultRetryTimeout":
+ return defaultRetryTimeout;
+ case "blockchain.rpcCallQuickRetryTimeout":
+ return quickRetryTimeout;
+ case "blockchain.rpcCallRetriesMaxTotalTimeout":
+ return retriesMaxTotalTimeout;
+ }
+ }),
+ });
+
+ stopRpcCallDurationMetricMock = jest.fn();
+ startRpcCallDurationMetricMock = jest.fn().mockReturnValue(stopRpcCallDurationMetricMock);
+
+ app = await Test.createTestingModule({
+ providers: [
+ BlockchainService,
+ {
+ provide: ConfigService,
+ useValue: configServiceMock,
+ },
+ {
+ provide: JsonRpcProviderBase,
+ useValue: provider,
+ },
+ {
+ provide: metricProviderKey,
+ useValue: {
+ startTimer: startRpcCallDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ blockchainService = app.get(BlockchainService);
+
+ blockchainService.bridgeAddresses = mock({
+ l2Erc20DefaultBridge: l2Erc20Bridge.toLowerCase(),
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("getL1BatchNumber", () => {
+ const batchNumber = 10;
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getL1BatchNumber").mockResolvedValue(batchNumber);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets batch number", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(provider.getL1BatchNumber).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchNumber" });
+ });
+
+ it("returns the batch number", async () => {
+ const result = await blockchainService.getL1BatchNumber();
+ expect(result).toEqual(batchNumber);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(provider.getL1BatchNumber).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchNumber" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getL1BatchNumber();
+ expect(result).toEqual(batchNumber);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection reset error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNRESET";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a network error", () => {
+ const error = new Error();
+ (error as any).code = "NETWORK_ERROR";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBatchDetails", () => {
+ const batchNumber = 10;
+ const batchDetails: types.BatchDetails = mock({ number: 10 });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getL1BatchDetails").mockResolvedValue(batchDetails);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets batch details by the specified batch number", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(provider.getL1BatchDetails).toHaveBeenCalledTimes(1);
+ expect(provider.getL1BatchDetails).toHaveBeenCalledWith(batchNumber);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchDetails" });
+ });
+
+ it("returns the batch details", async () => {
+ const result = await blockchainService.getL1BatchDetails(batchNumber);
+ expect(result).toEqual(batchDetails);
+ });
+
+ it("sets default committedAt, provenAt and executedAt for the very first batch", async () => {
+ jest.spyOn(provider, "getL1BatchDetails").mockResolvedValueOnce({ number: 0 } as types.BatchDetails);
+ const result = await blockchainService.getL1BatchDetails(0);
+ expect(result).toEqual({
+ number: 0,
+ committedAt: new Date(0),
+ provenAt: new Date(0),
+ executedAt: new Date(0),
+ });
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchDetails);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(provider.getL1BatchDetails).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getL1BatchDetails" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getL1BatchDetails(batchNumber);
+ expect(result).toEqual(batchDetails);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchDetails(batchNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchDetails(batchNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getL1BatchDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(batchDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getL1BatchDetails(batchNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getL1BatchDetails(batchNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBlock", () => {
+ const blockNumber = 10;
+ const block: types.Block = mock({ number: 10 });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getBlock").mockResolvedValue(block);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets block by the specified block number", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(provider.getBlock).toHaveBeenCalledTimes(1);
+ expect(provider.getBlock).toHaveBeenCalledWith(blockNumber);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlock" });
+ });
+
+ it("returns the block", async () => {
+ const result = await blockchainService.getBlock(blockNumber);
+ expect(result).toEqual(block);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlock")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(block);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(provider.getBlock).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlock" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBlock(blockNumber);
+ expect(result).toEqual(block);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlock(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlock")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(block);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlock(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlock")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(block);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlock(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlock(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBlockNumber", () => {
+ const blockNumber = 10;
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getBlockNumber").mockResolvedValue(blockNumber);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBlockNumber();
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets block number", async () => {
+ await blockchainService.getBlockNumber();
+ expect(provider.getBlockNumber).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBlockNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockNumber" });
+ });
+
+ it("returns the block number", async () => {
+ const result = await blockchainService.getBlockNumber();
+ expect(result).toEqual(blockNumber);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockNumber);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBlockNumber();
+ expect(provider.getBlockNumber).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getBlockNumber();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockNumber" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBlockNumber();
+ expect(result).toEqual(blockNumber);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockNumber")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockNumber);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockNumber();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockNumber()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getBlockDetails", () => {
+ const blockNumber = 10;
+ const blockDetails: types.BlockDetails = mock({ number: 10 });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getBlockDetails").mockResolvedValue(blockDetails);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets block details by the specified block number", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(provider.getBlockDetails).toHaveBeenCalledTimes(1);
+ expect(provider.getBlockDetails).toHaveBeenCalledWith(blockNumber);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockDetails" });
+ });
+
+ it("returns the block details", async () => {
+ const result = await blockchainService.getBlockDetails(blockNumber);
+ expect(result).toEqual(blockDetails);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockDetails);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(provider.getBlockDetails).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBlockDetails" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBlockDetails(blockNumber);
+ expect(result).toEqual(blockDetails);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockDetails(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockDetails(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBlockDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(blockDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBlockDetails(blockNumber);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBlockDetails(blockNumber)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getTransaction", () => {
+ const transactionHash = "transactionHash";
+ const transaction: types.TransactionResponse = mock({ hash: "transactionHash" });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getTransaction").mockResolvedValue(transaction);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction by the specified hash", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(provider.getTransaction).toHaveBeenCalledTimes(1);
+ expect(provider.getTransaction).toHaveBeenCalledWith(transactionHash);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransaction" });
+ });
+
+ it("returns the transaction", async () => {
+ const result = await blockchainService.getTransaction(transactionHash);
+ expect(result).toEqual(transaction);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransaction")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transaction);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(provider.getTransaction).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransaction" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getTransaction(transactionHash);
+ expect(result).toEqual(transaction);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransaction(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransaction")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transaction);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransaction(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransaction")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transaction);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransaction(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransaction(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getTransactionDetails", () => {
+ const transactionHash = "transactionHash";
+ const transactionDetails: types.TransactionDetails = mock({
+ initiatorAddress: "initiatorAddress",
+ });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getTransactionDetails").mockResolvedValue(transactionDetails);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction details by the specified hash", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(provider.getTransactionDetails).toHaveBeenCalledTimes(1);
+ expect(provider.getTransactionDetails).toHaveBeenCalledWith(transactionHash);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionDetails" });
+ });
+
+ it("returns the transaction details", async () => {
+ const result = await blockchainService.getTransactionDetails(transactionHash);
+ expect(result).toEqual(transactionDetails);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionDetails);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(provider.getTransactionDetails).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionDetails" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getTransactionDetails(transactionHash);
+ expect(result).toEqual(transactionDetails);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionDetails(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionDetails(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionDetails")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionDetails);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionDetails(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionDetails(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getTransactionReceipt", () => {
+ const transactionHash = "transactionHash";
+ const transactionReceipt: types.TransactionReceipt = mock({
+ transactionHash: "initiatorAddress",
+ });
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getTransactionReceipt").mockResolvedValue(transactionReceipt);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction receipt by the specified hash", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(provider.getTransactionReceipt).toHaveBeenCalledTimes(1);
+ expect(provider.getTransactionReceipt).toHaveBeenCalledWith(transactionHash);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionReceipt" });
+ });
+
+ it("returns the transaction receipt", async () => {
+ const result = await blockchainService.getTransactionReceipt(transactionHash);
+ expect(result).toEqual(transactionReceipt);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionReceipt);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(provider.getTransactionReceipt).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getTransactionReceipt" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getTransactionReceipt(transactionHash);
+ expect(result).toEqual(transactionReceipt);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionReceipt(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionReceipt);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionReceipt(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getTransactionReceipt")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(transactionReceipt);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getTransactionReceipt(transactionHash);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getTransactionReceipt(transactionHash)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getLogs", () => {
+ const fromBlock = 10;
+ const toBlock = 20;
+ const logs: types.Log[] = [mock({ logIndex: 1 }), mock({ logIndex: 2 })];
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getLogs").mockResolvedValue(logs);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets logs by the specified from and to block numbers", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(provider.getLogs).toHaveBeenCalledTimes(1);
+ expect(provider.getLogs).toHaveBeenCalledWith({ fromBlock, toBlock });
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getLogs" });
+ });
+
+ it("returns the logs", async () => {
+ const result = await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(result).toEqual(logs);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getLogs")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(logs);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(provider.getLogs).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getLogs" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(result).toEqual(logs);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getLogs({ fromBlock, toBlock })).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getLogs")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(logs);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getLogs({ fromBlock, toBlock })).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getLogs")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(logs);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getLogs({ fromBlock, toBlock });
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getLogs({ fromBlock, toBlock })).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getCode", () => {
+ const address = "address";
+ const bytecode = "0x0123345";
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getCode").mockResolvedValue(bytecode);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getCode(address);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets bytecode for the specified address", async () => {
+ await blockchainService.getCode(address);
+ expect(provider.getCode).toHaveBeenCalledTimes(1);
+ expect(provider.getCode).toHaveBeenCalledWith(address);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getCode(address);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getCode" });
+ });
+
+ it("returns the bytecode", async () => {
+ const result = await blockchainService.getCode(address);
+ expect(result).toEqual(bytecode);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getCode")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bytecode);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getCode(address);
+ expect(provider.getCode).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getCode(address);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getCode" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getCode(address);
+ expect(result).toEqual(bytecode);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getCode(address)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getCode")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bytecode);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getCode(address);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getCode(address)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getCode")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bytecode);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getCode(address);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getCode(address)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("getDefaultBridgeAddresses", () => {
+ const bridgeAddress = {
+ erc20L1: "erc20L1",
+ erc20L2: "erc20L2",
+ wethL1: "wethL1",
+ wethL2: "wethL2",
+ };
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "getDefaultBridgeAddresses").mockResolvedValue(bridgeAddress);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets bridge addresses", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(provider.getDefaultBridgeAddresses).toHaveBeenCalledTimes(1);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getDefaultBridgeAddresses" });
+ });
+
+ it("returns bridge addresses", async () => {
+ const result = await blockchainService.getDefaultBridgeAddresses();
+ expect(result).toEqual(bridgeAddress);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getDefaultBridgeAddresses")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bridgeAddress);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(provider.getDefaultBridgeAddresses).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getDefaultBridgeAddresses" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getDefaultBridgeAddresses();
+ expect(result).toEqual(bridgeAddress);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getDefaultBridgeAddresses()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getDefaultBridgeAddresses")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bridgeAddress);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getDefaultBridgeAddresses()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getDefaultBridgeAddresses")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(bridgeAddress);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getDefaultBridgeAddresses();
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getDefaultBridgeAddresses()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("on", () => {
+ beforeEach(() => {
+ provider.on = jest.fn();
+ });
+
+ it("subscribes to the new events", () => {
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ const handler = () => {};
+ blockchainService.on("block", handler);
+ expect(provider.on).toHaveBeenCalledTimes(1);
+ expect(provider.on).toHaveBeenCalledWith("block", handler);
+ });
+ });
+
+ describe("getERC20TokenData", () => {
+ const contractAddress = "contractAddress";
+ const symbol = "symbol";
+ const decimals = 18;
+ const name = "name";
+ let symbolMock: jest.Mock;
+ let decimalMock: jest.Mock;
+ let nameMock: jest.Mock;
+
+ beforeEach(() => {
+ symbolMock = jest.fn().mockResolvedValue(symbol);
+ decimalMock = jest.fn().mockResolvedValue(decimals);
+ nameMock = jest.fn().mockResolvedValue(name);
+
+ (RetryableContract as any as jest.Mock).mockReturnValue(
+ mock({
+ symbol: symbolMock,
+ decimals: decimalMock,
+ name: nameMock,
+ })
+ );
+ });
+
+ it("uses ERC20 token contract interface", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(RetryableContract).toHaveBeenCalledTimes(1);
+ expect(RetryableContract).toBeCalledWith(contractAddress, utils.IERC20, provider);
+ });
+
+ it("gets contact symbol", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(symbolMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets contact decimals", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(decimalMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets contact name", async () => {
+ await blockchainService.getERC20TokenData(contractAddress);
+ expect(nameMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns token data", async () => {
+ const tokenData = await blockchainService.getERC20TokenData(contractAddress);
+ expect(tokenData).toEqual({ symbol, decimals, name });
+ });
+
+ describe("when contract function throws an error", () => {
+ const error = new Error("contract error");
+
+ beforeEach(() => {
+ symbolMock = jest.fn().mockImplementation(() => {
+ throw error;
+ });
+ decimalMock = jest.fn().mockResolvedValue(decimals);
+ nameMock = jest.fn().mockResolvedValue(name);
+
+ (RetryableContract as any as jest.Mock).mockReturnValue(
+ mock({
+ symbol: symbolMock,
+ decimals: decimalMock,
+ name: nameMock,
+ })
+ );
+ });
+
+ it("throws an error", async () => {
+ await expect(blockchainService.getERC20TokenData(contractAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("getBalance", () => {
+ const blockNumber = 5;
+ let blockTag: string;
+ let tokenAddress: string;
+ const address = "address";
+
+ beforeEach(() => {
+ blockTag = "latest";
+ tokenAddress = "tokenAddress";
+ jest.spyOn(providerFormatterMock, "blockTag").mockReturnValueOnce(blockTag);
+ });
+
+ it("gets block tag for the specified blockNumber", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(providerFormatterMock.blockTag).toHaveBeenCalledTimes(1);
+ expect(providerFormatterMock.blockTag).toHaveBeenCalledWith(blockNumber);
+ });
+
+ describe("if token address is ETH", () => {
+ let timeoutSpy;
+ const balance = ethers.BigNumber.from(10);
+
+ beforeEach(() => {
+ tokenAddress = utils.ETH_ADDRESS;
+ jest.spyOn(provider, "getBalance").mockResolvedValue(ethers.BigNumber.from(10));
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets the balance for ETH", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(provider.getBalance).toHaveBeenCalledTimes(1);
+ expect(provider.getBalance).toHaveBeenCalledWith(address, blockTag);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBalance" });
+ });
+
+ it("returns the address balance for ETH", async () => {
+ jest.spyOn(provider, "getBalance").mockResolvedValueOnce(ethers.BigNumber.from(15));
+
+ const balance = await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(balance).toStrictEqual(balance);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(provider.getBalance).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "getBalance" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(result).toEqual(balance);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection reset error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNRESET";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a network error", () => {
+ const error = new Error();
+ (error as any).code = "NETWORK_ERROR";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "getBalance")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(balance);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("if token address is not ETH", () => {
+ beforeEach(() => {
+ tokenAddress = "0x22b44df5aa1ee4542b6318ff971f183135f5e4ce";
+ });
+
+ describe("if ERC20 Contract function throws an exception", () => {
+ const error = new Error("Ethers Contract error");
+
+ beforeEach(() => {
+ (RetryableContract as any as jest.Mock).mockReturnValueOnce(
+ mock({
+ balanceOf: jest.fn().mockImplementationOnce(() => {
+ throw error;
+ }),
+ })
+ );
+ });
+
+ it("throws an error", async () => {
+ await expect(blockchainService.getBalance(address, blockNumber, tokenAddress)).rejects.toThrowError(error);
+ });
+ });
+
+ describe("when there is a token with the specified address", () => {
+ let balanceOfMock: jest.Mock;
+
+ beforeEach(() => {
+ balanceOfMock = jest.fn().mockResolvedValueOnce(ethers.BigNumber.from(20));
+ (RetryableContract as any as jest.Mock).mockReturnValueOnce(
+ mock({
+ balanceOf: balanceOfMock,
+ })
+ );
+ });
+
+ it("uses the proper token contract", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(RetryableContract).toHaveBeenCalledTimes(1);
+ expect(RetryableContract).toBeCalledWith(tokenAddress, utils.IERC20, provider);
+ });
+
+ it("gets the balance for the specified address and block", async () => {
+ await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(balanceOfMock).toHaveBeenCalledTimes(1);
+ expect(balanceOfMock).toHaveBeenCalledWith(address, { blockTag });
+ });
+
+ it("returns the balance of the token", async () => {
+ const balance = await blockchainService.getBalance(address, blockNumber, tokenAddress);
+ expect(balance).toStrictEqual(ethers.BigNumber.from(20));
+ });
+ });
+ });
+ });
+
+ describe("debugTraceTransaction", () => {
+ const traceTransactionResult = {
+ type: "Call",
+ from: "0x0000000000000000000000000000000000000000",
+ to: "0x0000000000000000000000000000000000008001",
+ error: null,
+ revertReason: "Exceed daily limit",
+ };
+ let timeoutSpy;
+
+ beforeEach(() => {
+ jest.spyOn(provider, "send").mockResolvedValue(traceTransactionResult);
+ timeoutSpy = jest.spyOn(timersPromises, "setTimeout");
+ });
+
+ it("starts the rpc call duration metric", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(startRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets transaction trace", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(provider.send).toHaveBeenCalledTimes(1);
+ expect(provider.send).toHaveBeenCalledWith("debug_traceTransaction", [
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b",
+ {
+ tracer: "callTracer",
+ tracerConfig: { onlyTopCall: false },
+ },
+ ]);
+ });
+
+ it("gets transaction trace with only top call", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b",
+ true
+ );
+ expect(provider.send).toHaveBeenCalledTimes(1);
+ expect(provider.send).toHaveBeenCalledWith("debug_traceTransaction", [
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b",
+ {
+ tracer: "callTracer",
+ tracerConfig: { onlyTopCall: true },
+ },
+ ]);
+ });
+
+ it("stops the rpc call duration metric", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "debugTraceTransaction" });
+ });
+
+ it("returns transaction trace", async () => {
+ const result = await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(result).toEqual(traceTransactionResult);
+ });
+
+ describe("if the call throws an error", () => {
+ const error = new Error("RPC call error");
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "send")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(traceTransactionResult);
+ });
+
+ it("retries RPC call with a default timeout", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(provider.send).toHaveBeenCalledTimes(3);
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, defaultRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, defaultRetryTimeout);
+ });
+
+ it("stops the rpc call duration metric only for the successful retry", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledTimes(1);
+ expect(stopRpcCallDurationMetricMock).toHaveBeenCalledWith({ function: "debugTraceTransaction" });
+ });
+
+ it("returns result of the successful RPC call", async () => {
+ const result = await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(result).toEqual(traceTransactionResult);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(
+ blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ )
+ ).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a timeout error", () => {
+ const error = new Error();
+ (error as any).code = "TIMEOUT";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "send")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(traceTransactionResult);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(
+ blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ )
+ ).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("if the call throws a connection refused error", () => {
+ const error = new Error();
+ (error as any).code = "ECONNREFUSED";
+ beforeEach(() => {
+ jest
+ .spyOn(provider, "send")
+ .mockRejectedValueOnce(error)
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce(traceTransactionResult);
+ });
+
+ it("retries RPC call with a quick timeout", async () => {
+ await blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ );
+ expect(timeoutSpy).toHaveBeenCalledTimes(2);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(1, quickRetryTimeout);
+ expect(timeoutSpy).toHaveBeenNthCalledWith(2, quickRetryTimeout);
+ });
+
+ describe("and retries max total timeout is exceeded", () => {
+ beforeEach(() => {
+ (configServiceMock.get as jest.Mock).mockClear();
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(10);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(20);
+ (configServiceMock.get as jest.Mock).mockReturnValueOnce(1);
+
+ blockchainService = new BlockchainService(configServiceMock, provider, app.get(metricProviderKey));
+ });
+
+ it("stops retrying and throws the error", async () => {
+ await expect(
+ blockchainService.debugTraceTransaction(
+ "0xc0ae49e96910fa9df22eb59c0977905864664d495bc95906120695aa26e1710b"
+ )
+ ).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+
+ describe("onModuleInit", () => {
+ let bridgeAddresses;
+ beforeEach(() => {
+ bridgeAddresses = {
+ erc20L1: "l1Erc20DefaultBridge",
+ erc20L2: "l2Erc20DefaultBridge",
+ };
+
+ jest.spyOn(provider, "getDefaultBridgeAddresses").mockResolvedValueOnce(bridgeAddresses);
+ });
+
+ it("inits L2 ERC20 bridge address", async () => {
+ await blockchainService.onModuleInit();
+ expect(blockchainService.bridgeAddresses.l2Erc20DefaultBridge).toBe(bridgeAddresses.erc20L2.toLowerCase());
+ });
+
+ it("inits L1 ERC20 bridge address", async () => {
+ await blockchainService.onModuleInit();
+ expect(blockchainService.bridgeAddresses.l1Erc20DefaultBridge).toBe(bridgeAddresses.erc20L1.toLowerCase());
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/blockchain/blockchain.service.ts b/packages/data-fetcher/src/blockchain/blockchain.service.ts
new file mode 100644
index 0000000000..4b3cd8d8d3
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/blockchain.service.ts
@@ -0,0 +1,193 @@
+import { Injectable, OnModuleInit, Logger } from "@nestjs/common";
+import { BigNumber } from "ethers";
+import { utils, types } from "zksync-web3";
+import { Histogram } from "prom-client";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { EventType, Listener } from "@ethersproject/abstract-provider";
+import { ConfigService } from "@nestjs/config";
+import { setTimeout } from "timers/promises";
+import { JsonRpcProviderBase } from "../rpcProvider";
+import { BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME, BlockchainRpcCallMetricLabel } from "../metrics";
+import { RetryableContract } from "./retryableContract";
+
+export interface BridgeAddresses {
+ l1Erc20DefaultBridge: string;
+ l2Erc20DefaultBridge: string;
+}
+
+export interface TraceTransactionResult {
+ type: string;
+ from: string;
+ to: string;
+ error: string | null;
+ revertReason: string | null;
+}
+
+@Injectable()
+export class BlockchainService implements OnModuleInit {
+ private readonly logger: Logger;
+ private readonly rpcCallsDefaultRetryTimeout: number;
+ private readonly rpcCallsQuickRetryTimeout: number;
+ private readonly rpcCallRetriesMaxTotalTimeout: number;
+ private readonly errorCodesForQuickRetry: string[] = ["NETWORK_ERROR", "ECONNRESET", "ECONNREFUSED", "TIMEOUT"];
+ public bridgeAddresses: BridgeAddresses;
+
+ public constructor(
+ configService: ConfigService,
+ private readonly provider: JsonRpcProviderBase,
+ @InjectMetric(BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME)
+ private readonly rpcCallDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(BlockchainService.name);
+ this.rpcCallsDefaultRetryTimeout = configService.get("blockchain.rpcCallDefaultRetryTimeout");
+ this.rpcCallsQuickRetryTimeout = configService.get("blockchain.rpcCallQuickRetryTimeout");
+ this.rpcCallRetriesMaxTotalTimeout = configService.get("blockchain.rpcCallRetriesMaxTotalTimeout");
+ }
+
+ private async rpcCall(action: () => Promise, functionName: string, retriesTotalTimeAwaited = 0): Promise {
+ const stopDurationMeasuring = this.rpcCallDurationMetric.startTimer();
+ try {
+ const result = await action();
+ stopDurationMeasuring({ function: functionName });
+ return result;
+ } catch (error) {
+ this.logger.error({ message: error.message, code: error.code }, error.stack);
+ const retryTimeout = this.errorCodesForQuickRetry.includes(error.code)
+ ? this.rpcCallsQuickRetryTimeout
+ : this.rpcCallsDefaultRetryTimeout;
+
+ const totalTimeAwaited = retriesTotalTimeAwaited + retryTimeout;
+ if (totalTimeAwaited > this.rpcCallRetriesMaxTotalTimeout) {
+ this.logger.error({ message: "Exceeded retries total timeout, failing the request", functionName });
+ throw error;
+ }
+
+ await setTimeout(retryTimeout);
+ return this.rpcCall(action, functionName, totalTimeAwaited);
+ }
+ }
+
+ public async getL1BatchNumber(): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getL1BatchNumber();
+ }, "getL1BatchNumber");
+ }
+
+ public async getL1BatchDetails(batchNumber: number): Promise {
+ return await this.rpcCall(async () => {
+ const batchDetails = await this.provider.getL1BatchDetails(batchNumber);
+ if (batchDetails && batchNumber === 0) {
+ batchDetails.committedAt = batchDetails.provenAt = batchDetails.executedAt = new Date(0);
+ }
+ return batchDetails;
+ }, "getL1BatchDetails");
+ }
+
+ public async getBlock(blockHashOrBlockTag: types.BlockTag): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBlock(blockHashOrBlockTag);
+ }, "getBlock");
+ }
+
+ public async getBlockNumber(): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBlockNumber();
+ }, "getBlockNumber");
+ }
+
+ public async getBlockDetails(blockNumber: number): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBlockDetails(blockNumber);
+ }, "getBlockDetails");
+ }
+
+ public async getTransaction(transactionHash: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getTransaction(transactionHash);
+ }, "getTransaction");
+ }
+
+ public async getTransactionDetails(transactionHash: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getTransactionDetails(transactionHash);
+ }, "getTransactionDetails");
+ }
+
+ public async getTransactionReceipt(transactionHash: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getTransactionReceipt(transactionHash);
+ }, "getTransactionReceipt");
+ }
+
+ public async getLogs(eventFilter: { fromBlock: number; toBlock: number }): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getLogs(eventFilter);
+ }, "getLogs");
+ }
+
+ public async getCode(address: string): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.getCode(address);
+ }, "getCode");
+ }
+
+ public async getDefaultBridgeAddresses(): Promise<{ erc20L1: string; erc20L2: string }> {
+ return await this.rpcCall(async () => {
+ return await this.provider.getDefaultBridgeAddresses();
+ }, "getDefaultBridgeAddresses");
+ }
+
+ public async debugTraceTransaction(txHash: string, onlyTopCall = false): Promise {
+ return await this.rpcCall(async () => {
+ return await this.provider.send("debug_traceTransaction", [
+ txHash,
+ {
+ tracer: "callTracer",
+ tracerConfig: { onlyTopCall },
+ },
+ ]);
+ }, "debugTraceTransaction");
+ }
+
+ public async on(eventName: EventType, listener: Listener): Promise {
+ this.provider.on(eventName, listener);
+ }
+
+ public async getERC20TokenData(contractAddress: string): Promise<{ symbol: string; decimals: number; name: string }> {
+ const erc20Contract = new RetryableContract(contractAddress, utils.IERC20, this.provider);
+ const [symbol, decimals, name] = await Promise.all([
+ erc20Contract.symbol(),
+ erc20Contract.decimals(),
+ erc20Contract.name(),
+ ]);
+ return {
+ symbol,
+ decimals,
+ name,
+ };
+ }
+
+ public async getBalance(address: string, blockNumber: number, tokenAddress: string): Promise {
+ const blockTag = this.provider.formatter.blockTag(blockNumber);
+
+ if (utils.isETH(tokenAddress)) {
+ return await this.rpcCall(async () => {
+ return await this.provider.getBalance(address, blockTag);
+ }, "getBalance");
+ }
+
+ const erc20Contract = new RetryableContract(tokenAddress, utils.IERC20, this.provider);
+ return await erc20Contract.balanceOf(address, { blockTag });
+ }
+
+ public async onModuleInit(): Promise {
+ const bridgeAddresses = await this.getDefaultBridgeAddresses();
+
+ this.bridgeAddresses = {
+ l1Erc20DefaultBridge: bridgeAddresses.erc20L1.toLowerCase(),
+ l2Erc20DefaultBridge: bridgeAddresses.erc20L2.toLowerCase(),
+ };
+
+ this.logger.debug(`L2 ERC20 Bridge is set to: ${this.bridgeAddresses.l2Erc20DefaultBridge}`);
+ }
+}
diff --git a/packages/data-fetcher/src/blockchain/index.ts b/packages/data-fetcher/src/blockchain/index.ts
new file mode 100644
index 0000000000..ef903b3fc2
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/index.ts
@@ -0,0 +1 @@
+export * from "./blockchain.service";
diff --git a/packages/data-fetcher/src/blockchain/retryableContract.spec.ts b/packages/data-fetcher/src/blockchain/retryableContract.spec.ts
new file mode 100644
index 0000000000..cfbce3611b
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/retryableContract.spec.ts
@@ -0,0 +1,267 @@
+import * as ethers from "ethers";
+import { mock } from "jest-mock-extended";
+import { utils } from "zksync-web3";
+import { setTimeout } from "timers/promises";
+import { RetryableContract } from "./retryableContract";
+
+jest.mock("../config", () => ({
+ default: () => ({
+ blockchain: {
+ rpcCallRetriesMaxTotalTimeout: 200000,
+ },
+ }),
+}));
+
+jest.mock("ethers", () => ({
+ ...jest.requireActual("ethers"),
+ Contract: jest.fn(),
+}));
+
+jest.mock("@nestjs/common", () => ({
+ Logger: jest.fn().mockReturnValue({
+ error: jest.fn(),
+ debug: jest.fn(),
+ warn: jest.fn(),
+ }),
+}));
+
+jest.mock("timers/promises", () => ({
+ setTimeout: jest.fn().mockResolvedValue(null),
+}));
+
+describe("RetryableContract", () => {
+ const tokenAddress = "tokenAddress";
+ const providerMock = mock({});
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("constructor", () => {
+ it("inits Contract instance with specified ctor params", async () => {
+ new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ expect(ethers.Contract).toHaveBeenCalledTimes(1);
+ expect(ethers.Contract).toBeCalledWith(tokenAddress, utils.IERC20, providerMock);
+ });
+ });
+
+ describe("contract field access", () => {
+ const fieldValue = "fieldValue";
+ let contract: RetryableContract;
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractField: fieldValue,
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("returns field value", () => {
+ const result = contract.contractField;
+ expect(result).toBe(fieldValue);
+ });
+ });
+
+ describe("contract sync function call", () => {
+ const functionResult = "functionResult";
+ let contract: RetryableContract;
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: () => functionResult,
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("returns function call result", () => {
+ const result = contract.contractFn();
+ expect(result).toBe(functionResult);
+ });
+ });
+
+ describe("contract async function call", () => {
+ const functionResult = "functionResult";
+ let contract: RetryableContract;
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => functionResult,
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("returns function call async result", async () => {
+ const result = await contract.contractFn();
+ expect(result).toBe(functionResult);
+ });
+
+ describe("when throws a permanent call exception function error", () => {
+ const callExceptionError = {
+ code: "CALL_EXCEPTION",
+ method: "contractFn(address)",
+ transaction: {
+ data: "0x00",
+ to: "to",
+ },
+ message: "call revert exception ....",
+ };
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ throw callExceptionError;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("throws an error", async () => {
+ expect.assertions(1);
+
+ try {
+ await contract.contractFn();
+ } catch (e) {
+ expect(e).toBe(callExceptionError);
+ }
+ });
+ });
+
+ describe("when throws an invalid argument function error", () => {
+ const invalidArgumentError = {
+ code: "INVALID_ARGUMENT",
+ };
+
+ beforeEach(() => {
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ throw invalidArgumentError;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock);
+ });
+
+ it("throws an error", async () => {
+ expect.assertions(1);
+
+ try {
+ await contract.contractFn();
+ } catch (e) {
+ expect(e).toBe(invalidArgumentError);
+ }
+ });
+ });
+
+ describe("when throws a few network errors before returning a result", () => {
+ const functionResult = "functionResult";
+ const error = new Error();
+ (error as any).code = "NETWORK_ERROR";
+
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ if (countOfFailedRequests++ < 4) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("retries and returns the result when it's available", async () => {
+ const result = await contract.contractFn();
+ expect(result).toBe(functionResult);
+ expect(setTimeout).toBeCalledTimes(4);
+ expect(setTimeout).toBeCalledWith(20000);
+ expect(setTimeout).toBeCalledWith(40000);
+ expect(setTimeout).toBeCalledWith(60000);
+ expect(setTimeout).toBeCalledWith(60000);
+ });
+
+ describe("and retries total time exceeds the retries total max timeout", () => {
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ if (countOfFailedRequests++ < 5) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("throws an error", async () => {
+ await expect(contract.contractFn()).rejects.toThrowError(error);
+ });
+ });
+ });
+
+ describe("when throws a few errors with no method or message before returning a result", () => {
+ const functionResult = "functionResult";
+ const error = new Error();
+ (error as any).code = "CALL_EXCEPTION";
+
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ countOfFailedRequests++;
+ if (countOfFailedRequests <= 2) {
+ throw {
+ ...error,
+ transaction: {},
+ method: "contractFn()",
+ };
+ }
+ if (countOfFailedRequests <= 4) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("retries and returns the result when it's available", async () => {
+ const result = await contract.contractFn();
+ expect(result).toBe(functionResult);
+ expect(setTimeout).toBeCalledTimes(4);
+ expect(setTimeout).toBeCalledWith(20000);
+ expect(setTimeout).toBeCalledWith(40000);
+ expect(setTimeout).toBeCalledWith(60000);
+ expect(setTimeout).toBeCalledWith(60000);
+ });
+
+ describe("and retries total time exceeds the retries total max timeout", () => {
+ beforeEach(() => {
+ let countOfFailedRequests = 0;
+ (ethers.Contract as any as jest.Mock).mockReturnValue({
+ contractFn: async () => {
+ if (countOfFailedRequests++ < 5) {
+ throw error;
+ }
+ return functionResult;
+ },
+ });
+
+ contract = new RetryableContract(tokenAddress, utils.IERC20, providerMock, 20000);
+ });
+
+ it("throws an error", async () => {
+ await expect(contract.contractFn()).rejects.toThrowError(error);
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/blockchain/retryableContract.ts b/packages/data-fetcher/src/blockchain/retryableContract.ts
new file mode 100644
index 0000000000..846b2ca5f2
--- /dev/null
+++ b/packages/data-fetcher/src/blockchain/retryableContract.ts
@@ -0,0 +1,124 @@
+import { Logger } from "@nestjs/common";
+import { Provider } from "@ethersproject/abstract-provider";
+import { setTimeout } from "timers/promises";
+import { Contract, ContractInterface, Signer, errors } from "ethers";
+import config from "../config";
+
+const { blockchain } = config();
+
+interface EthersError {
+ code: string;
+ method: string;
+ transaction: {
+ data: string;
+ to: string;
+ };
+ message: string;
+}
+
+const MAX_RETRY_INTERVAL = 60000;
+
+const PERMANENT_ERRORS: string[] = [
+ errors.INVALID_ARGUMENT,
+ errors.MISSING_ARGUMENT,
+ errors.UNEXPECTED_ARGUMENT,
+ errors.NOT_IMPLEMENTED,
+];
+
+const shouldRetry = (calledFunctionName: string, error: EthersError): boolean => {
+ return (
+ !PERMANENT_ERRORS.includes(error.code) &&
+ !(
+ error.code === errors.CALL_EXCEPTION &&
+ error.method?.startsWith(`${calledFunctionName}(`) &&
+ !!error.transaction &&
+ error.message?.startsWith("call revert exception")
+ )
+ );
+};
+
+const retryableFunctionCall = async (
+ result: Promise,
+ functionCall: () => any,
+ logger: Logger,
+ functionName: string,
+ addressOrName: string,
+ retryTimeout: number,
+ retriesTotalTimeAwaited = 0
+): Promise => {
+ try {
+ return await result;
+ } catch (error) {
+ const isRetryable = shouldRetry(functionName, error);
+ if (!isRetryable) {
+ logger.warn({
+ message: `Requested contract function ${functionName} failed to execute, not retryable`,
+ contractAddress: addressOrName,
+ error,
+ });
+ throw error;
+ }
+
+ const exceededRetriesTotalTimeout =
+ retriesTotalTimeAwaited + retryTimeout > blockchain.rpcCallRetriesMaxTotalTimeout;
+ const failedStatus = exceededRetriesTotalTimeout ? "exceeded total retries timeout" : "retrying...";
+ logger.warn({
+ message: `Requested contract function ${functionName} failed to execute, ${failedStatus}`,
+ contractAddress: addressOrName,
+ error,
+ });
+
+ if (exceededRetriesTotalTimeout) {
+ throw error;
+ }
+ }
+ await setTimeout(retryTimeout);
+
+ const nextRetryTimeout = Math.min(retryTimeout * 2, MAX_RETRY_INTERVAL);
+ return retryableFunctionCall(
+ functionCall(),
+ functionCall,
+ logger,
+ functionName,
+ addressOrName,
+ nextRetryTimeout,
+ retriesTotalTimeAwaited + retryTimeout
+ );
+};
+
+const getProxyHandler = (addressOrName: string, logger: Logger, retryTimeout: number) => {
+ return {
+ get: function (target, propertyKey, receiver) {
+ if (target.contract[propertyKey] instanceof Function) {
+ return function (...args) {
+ const result = target.contract[propertyKey].apply(this, args);
+ if (result instanceof Promise) {
+ return retryableFunctionCall(
+ result,
+ () => target.contract[propertyKey].apply(this, args),
+ logger,
+ propertyKey,
+ addressOrName,
+ retryTimeout
+ );
+ }
+ return result;
+ };
+ }
+ return Reflect.get(target.contract, propertyKey, receiver);
+ },
+ };
+};
+
+export class RetryableContract extends Contract {
+ constructor(
+ addressOrName: string,
+ contractInterface: ContractInterface,
+ signerOrProvider: Signer | Provider,
+ retryTimeout = 1000
+ ) {
+ const logger = new Logger("Contract");
+ super(addressOrName, contractInterface, signerOrProvider);
+ return new Proxy({ contract: this }, getProxyHandler(addressOrName, logger, retryTimeout));
+ }
+}
diff --git a/packages/data-fetcher/src/common/interceptors/responseTransform.interceptor.spec.ts b/packages/data-fetcher/src/common/interceptors/responseTransform.interceptor.spec.ts
new file mode 100644
index 0000000000..fe609734ed
--- /dev/null
+++ b/packages/data-fetcher/src/common/interceptors/responseTransform.interceptor.spec.ts
@@ -0,0 +1,69 @@
+import { ResponseTransformInterceptor } from "./responseTransform.interceptor";
+import { of } from "rxjs";
+import { BigNumber } from "ethers";
+
+const interceptor = new ResponseTransformInterceptor();
+
+describe("ResponseTransformInterceptor", () => {
+ const incomingData = [
+ {
+ block: {
+ hash: "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ number: 14360,
+ gasLimit: BigNumber.from("0xffffffff"),
+ gasUsed: BigNumber.from("0x035436"),
+ transactions: ["0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2"],
+ },
+ },
+ {
+ block: {
+ hash: "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ number: 14361,
+ gasLimit: BigNumber.from("0xffffffff"),
+ gasUsed: BigNumber.from("0x02afe7"),
+ transactions: ["0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e"],
+ },
+ },
+ ];
+
+ it("transforms the data", (done) => {
+ expect.assertions(1);
+ const interceptorObservable = interceptor.intercept({} as any, { handle: () => of(incomingData) });
+ interceptorObservable.subscribe({
+ next: (val) => {
+ expect(val).toEqual([
+ {
+ block: {
+ hash: "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ number: 14360,
+ gasLimit: "4294967295",
+ gasUsed: "218166",
+ transactions: ["0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2"],
+ },
+ },
+ {
+ block: {
+ hash: "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ number: 14361,
+ gasLimit: "4294967295",
+ gasUsed: "176103",
+ transactions: ["0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e"],
+ },
+ },
+ ]);
+ },
+ complete: () => done(),
+ });
+ });
+
+ it("returns null for null incoming data", (done) => {
+ expect.assertions(1);
+ const interceptorObservable = interceptor.intercept({} as any, { handle: () => of(null) });
+ interceptorObservable.subscribe({
+ next: (val) => {
+ expect(val).toBeNull();
+ },
+ complete: () => done(),
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/common/interceptors/responseTransform.interceptor.ts b/packages/data-fetcher/src/common/interceptors/responseTransform.interceptor.ts
new file mode 100644
index 0000000000..a13b90244e
--- /dev/null
+++ b/packages/data-fetcher/src/common/interceptors/responseTransform.interceptor.ts
@@ -0,0 +1,28 @@
+import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
+import { Observable } from "rxjs";
+import { map } from "rxjs/operators";
+import { BigNumber } from "ethers";
+
+const transform = (obj: any): any => {
+ if (obj == null) {
+ return obj;
+ }
+ if (obj instanceof Array) {
+ obj.forEach(transform);
+ }
+ for (const propName in obj) {
+ if (obj[propName] instanceof BigNumber) {
+ obj[propName] = obj[propName].toString();
+ } else if (obj[propName] instanceof Object) {
+ transform(obj[propName]);
+ }
+ }
+ return obj;
+};
+
+@Injectable()
+export class ResponseTransformInterceptor implements NestInterceptor {
+ intercept(_: ExecutionContext, next: CallHandler): Observable {
+ return next.handle().pipe(map((data) => transform(data)));
+ }
+}
diff --git a/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.spec.ts b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.spec.ts
new file mode 100644
index 0000000000..82da2e28ea
--- /dev/null
+++ b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.spec.ts
@@ -0,0 +1,84 @@
+import { BadRequestException } from "@nestjs/common";
+import { ParseLimitedIntPipe } from "./parseLimitedInt.pipe";
+
+describe("ParseLimitedIntPipe", () => {
+ describe("transform", () => {
+ describe("throws a BadRequestException", () => {
+ it("if specified input is not valid", async () => {
+ const pipe = new ParseLimitedIntPipe();
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("invalidAddressParam");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed (numeric string is expected)");
+ }
+ });
+
+ it("if specified value is less than specified min value", async () => {
+ const pipe = new ParseLimitedIntPipe({ min: 1, max: 10 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("0");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed: specified int is out of defined boundaries: [1;10].");
+ }
+ });
+
+ it("if specified value is higher than specified max value", async () => {
+ const pipe = new ParseLimitedIntPipe({ min: 1, max: 10 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("11");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed: specified int is out of defined boundaries: [1;10].");
+ }
+ });
+
+ it("if no min option specified uses 0 as a min value", async () => {
+ const pipe = new ParseLimitedIntPipe({ max: 10 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("-10");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe("Validation failed: specified int is out of defined boundaries: [0;10].");
+ }
+ });
+
+ it("if no max option specified uses max int number as a max value", async () => {
+ const pipe = new ParseLimitedIntPipe({ min: 1 });
+ expect.assertions(2);
+
+ try {
+ await pipe.transform("9007199254740992");
+ } catch (error) {
+ expect(error).toBeInstanceOf(BadRequestException);
+ expect(error.message).toBe(
+ "Validation failed: specified int is out of defined boundaries: [1;9007199254740991]."
+ );
+ }
+ });
+ });
+
+ it("returns undefined when value is not defined and isOptional set to true", async () => {
+ const pipe = new ParseLimitedIntPipe({ isOptional: true });
+
+ const parsedInt = await pipe.transform(null);
+ expect(parsedInt).toBe(undefined);
+ });
+
+ it("returns parsed value", async () => {
+ const pipe = new ParseLimitedIntPipe();
+
+ const parsedInt = await pipe.transform("10");
+ expect(parsedInt).toBe(10);
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.ts b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.ts
new file mode 100644
index 0000000000..4a804417ec
--- /dev/null
+++ b/packages/data-fetcher/src/common/pipes/parseLimitedInt.pipe.ts
@@ -0,0 +1,34 @@
+import { ArgumentMetadata, ParseIntPipe, ParseIntPipeOptions } from "@nestjs/common";
+
+declare interface ParseLimitedIntPipeOptions extends ParseIntPipeOptions {
+ min?: number;
+ max?: number;
+ isOptional?: boolean;
+}
+
+export class ParseLimitedIntPipe extends ParseIntPipe {
+ constructor(private readonly options?: ParseLimitedIntPipeOptions) {
+ super(options);
+ }
+
+ public override async transform(value: string | number, metadata?: ArgumentMetadata): Promise {
+ if (!value && value !== 0 && this.options.isOptional) {
+ return undefined;
+ }
+ const parsedInt = await super.transform(value as string, metadata);
+ let { min, max } = this.options || {};
+
+ if (isNaN(min)) {
+ min = 0;
+ }
+ if (isNaN(max)) {
+ max = Number.MAX_SAFE_INTEGER;
+ }
+
+ if (parsedInt < min || parsedInt > max) {
+ throw this.exceptionFactory(`Validation failed: specified int is out of defined boundaries: [${min};${max}].`);
+ }
+
+ return parsedInt;
+ }
+}
diff --git a/packages/data-fetcher/src/config.spec.ts b/packages/data-fetcher/src/config.spec.ts
new file mode 100644
index 0000000000..b519910372
--- /dev/null
+++ b/packages/data-fetcher/src/config.spec.ts
@@ -0,0 +1,30 @@
+import config from "./config";
+
+describe("config", () => {
+ const env = process.env;
+
+ beforeAll(() => {
+ process.env = {};
+ });
+
+ afterAll(() => {
+ process.env = env;
+ });
+
+ it("sets default values", () => {
+ expect(config()).toEqual({
+ port: 3040,
+ blockchain: {
+ rpcUrl: "http://localhost:3050",
+ rpcCallDefaultRetryTimeout: 30000,
+ rpcCallQuickRetryTimeout: 500,
+ rpcCallRetriesMaxTotalTimeout: 90000,
+ rpcCallConnectionTimeout: 20000,
+ rpcCallConnectionQuickTimeout: 10000,
+ wsMaxConnections: 5,
+ useWebSocketsForTransactions: false,
+ },
+ maxBlocksBatchSize: 20,
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/config.ts b/packages/data-fetcher/src/config.ts
new file mode 100644
index 0000000000..da399ecc91
--- /dev/null
+++ b/packages/data-fetcher/src/config.ts
@@ -0,0 +1,35 @@
+import { config } from "dotenv";
+config();
+
+export default () => {
+ const {
+ PORT,
+ BLOCKCHAIN_RPC_URL,
+ RPC_CALLS_DEFAULT_RETRY_TIMEOUT,
+ RPC_CALLS_QUICK_RETRY_TIMEOUT,
+ RPC_CALLS_RETRIES_MAX_TOTAL_TIMEOUT,
+ RPC_CALLS_CONNECTION_TIMEOUT,
+ RPC_CALLS_CONNECTION_QUICK_TIMEOUT,
+ WS_MAX_CONNECTIONS,
+ USE_WEBSOCKETS_FOR_TRANSACTIONS,
+ MAX_BLOCKS_BATCH_SIZE,
+ } = process.env;
+
+ return {
+ port: parseInt(PORT, 10) || 3040,
+ blockchain: {
+ rpcUrl: BLOCKCHAIN_RPC_URL || "http://localhost:3050",
+
+ rpcCallDefaultRetryTimeout: parseInt(RPC_CALLS_DEFAULT_RETRY_TIMEOUT, 10) || 30000,
+ rpcCallQuickRetryTimeout: parseInt(RPC_CALLS_QUICK_RETRY_TIMEOUT, 10) || 500,
+ rpcCallRetriesMaxTotalTimeout: parseInt(RPC_CALLS_RETRIES_MAX_TOTAL_TIMEOUT, 10) || 90000,
+
+ rpcCallConnectionTimeout: parseInt(RPC_CALLS_CONNECTION_TIMEOUT, 10) || 20000,
+ rpcCallConnectionQuickTimeout: parseInt(RPC_CALLS_CONNECTION_QUICK_TIMEOUT, 10) || 10000,
+
+ wsMaxConnections: parseInt(WS_MAX_CONNECTIONS, 10) || 5,
+ useWebSocketsForTransactions: USE_WEBSOCKETS_FOR_TRANSACTIONS === "true",
+ },
+ maxBlocksBatchSize: parseInt(MAX_BLOCKS_BATCH_SIZE, 10) || 20,
+ };
+};
diff --git a/packages/data-fetcher/src/constants.ts b/packages/data-fetcher/src/constants.ts
new file mode 100644
index 0000000000..2346236d6f
--- /dev/null
+++ b/packages/data-fetcher/src/constants.ts
@@ -0,0 +1,36 @@
+import { utils } from "ethers";
+import { abi as ethTokenAbi } from "zksync-web3/abi/IEthToken.json";
+import { abi as erc20Abi } from "zksync-web3/abi/IERC20.json";
+import { abi as l2BridgeAbi } from "zksync-web3/abi/IL2Bridge.json";
+import * as erc721Abi from "./abis/erc721.json";
+import * as transferEventWithNoIndexesAbi from "./abis/transferEventWithNoIndexes.json";
+import * as l2StandardERC20Abi from "./abis/l2StandardERC20.json";
+
+export const ZERO_HASH_64 = "0x0000000000000000000000000000000000000000000000000000000000000000";
+
+export const CONTRACT_INTERFACES = {
+ ERC20: {
+ interface: new utils.Interface(erc20Abi),
+ abi: erc20Abi,
+ },
+ ERC721: {
+ interface: new utils.Interface(erc721Abi),
+ abi: erc721Abi,
+ },
+ L2_STANDARD_ERC20: {
+ interface: new utils.Interface(l2StandardERC20Abi),
+ abi: l2StandardERC20Abi,
+ },
+ TRANSFER_WITH_NO_INDEXES: {
+ interface: new utils.Interface(transferEventWithNoIndexesAbi),
+ abi: transferEventWithNoIndexesAbi,
+ },
+ ETH_TOKEN: {
+ interface: new utils.Interface(ethTokenAbi),
+ abi: ethTokenAbi,
+ },
+ L2_BRIDGE: {
+ interface: new utils.Interface(l2BridgeAbi),
+ abi: l2BridgeAbi,
+ },
+};
diff --git a/packages/data-fetcher/src/health/health.controller.spec.ts b/packages/data-fetcher/src/health/health.controller.spec.ts
new file mode 100644
index 0000000000..065fcd36cb
--- /dev/null
+++ b/packages/data-fetcher/src/health/health.controller.spec.ts
@@ -0,0 +1,91 @@
+import { ServiceUnavailableException, Logger } from "@nestjs/common";
+import { Test, TestingModule } from "@nestjs/testing";
+import { HealthCheckService, HealthCheckResult } from "@nestjs/terminus";
+import { mock } from "jest-mock-extended";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+import { HealthController } from "./health.controller";
+
+describe("HealthController", () => {
+ let healthCheckServiceMock: HealthCheckService;
+ let jsonRpcHealthIndicatorMock: JsonRpcHealthIndicator;
+ let healthController: HealthController;
+
+ beforeEach(async () => {
+ healthCheckServiceMock = mock({
+ check: jest.fn().mockImplementation((healthChecks) => {
+ for (const healthCheck of healthChecks) {
+ healthCheck();
+ }
+ }),
+ });
+
+ jsonRpcHealthIndicatorMock = mock();
+
+ const app: TestingModule = await Test.createTestingModule({
+ controllers: [HealthController],
+ providers: [
+ {
+ provide: HealthCheckService,
+ useValue: healthCheckServiceMock,
+ },
+ {
+ provide: JsonRpcHealthIndicator,
+ useValue: jsonRpcHealthIndicatorMock,
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ healthController = app.get(HealthController);
+ });
+
+ describe("check", () => {
+ it("checks health of the JSON RPC provider", async () => {
+ await healthController.check();
+ expect(jsonRpcHealthIndicatorMock.isHealthy).toHaveBeenCalledTimes(1);
+ expect(jsonRpcHealthIndicatorMock.isHealthy).toHaveBeenCalledWith("jsonRpcProvider");
+ });
+
+ it("returns the overall check status", async () => {
+ const healthCheckResult = mock({ status: "ok" });
+ jest.spyOn(healthCheckServiceMock, "check").mockResolvedValueOnce(healthCheckResult);
+ const result = await healthController.check();
+ expect(result).toBe(healthCheckResult);
+ });
+
+ describe("when health checks fail with an error", () => {
+ const error: ServiceUnavailableException = new ServiceUnavailableException({
+ status: "error",
+ rpc: {
+ status: "down",
+ },
+ });
+
+ beforeEach(() => {
+ jest.spyOn(healthCheckServiceMock, "check").mockImplementation(() => {
+ throw error;
+ });
+ });
+
+ it("throws generated error", async () => {
+ expect.assertions(4);
+ try {
+ await healthController.check();
+ } catch (e) {
+ expect(e).toBeInstanceOf(ServiceUnavailableException);
+ expect(e.message).toBe("Service Unavailable Exception");
+ expect(e.response).toEqual(error.getResponse());
+ expect(e.stack).toEqual(error.stack);
+ }
+ });
+ });
+ });
+
+ describe("onApplicationShutdown", () => {
+ it("defined and returns void", async () => {
+ const result = healthController.onApplicationShutdown();
+ expect(result).toBeUndefined();
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/health/health.controller.ts b/packages/data-fetcher/src/health/health.controller.ts
new file mode 100644
index 0000000000..0568b6c115
--- /dev/null
+++ b/packages/data-fetcher/src/health/health.controller.ts
@@ -0,0 +1,34 @@
+import { Logger, Controller, Get, OnApplicationShutdown } from "@nestjs/common";
+import { HealthCheckService, HealthCheck, HealthCheckResult } from "@nestjs/terminus";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+
+@Controller(["health", "ready"])
+export class HealthController implements OnApplicationShutdown {
+ private readonly logger: Logger;
+
+ constructor(
+ private readonly healthCheckService: HealthCheckService,
+ private readonly jsonRpcHealthIndicator: JsonRpcHealthIndicator
+ ) {
+ this.logger = new Logger(HealthController.name);
+ }
+
+ @Get()
+ @HealthCheck()
+ public async check(): Promise {
+ try {
+ const healthCheckResult = await this.healthCheckService.check([
+ () => this.jsonRpcHealthIndicator.isHealthy("jsonRpcProvider"),
+ ]);
+ this.logger.debug({ message: "Health check result", ...healthCheckResult });
+ return healthCheckResult;
+ } catch (error) {
+ this.logger.error({ message: error.message, response: error.getResponse() }, error.stack);
+ throw error;
+ }
+ }
+
+ onApplicationShutdown(signal?: string): void {
+ this.logger.debug({ message: "Received a signal", signal });
+ }
+}
diff --git a/packages/data-fetcher/src/health/health.module.ts b/packages/data-fetcher/src/health/health.module.ts
new file mode 100644
index 0000000000..dae128825d
--- /dev/null
+++ b/packages/data-fetcher/src/health/health.module.ts
@@ -0,0 +1,11 @@
+import { Module } from "@nestjs/common";
+import { TerminusModule } from "@nestjs/terminus";
+import { HealthController } from "./health.controller";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+
+@Module({
+ controllers: [HealthController],
+ imports: [TerminusModule],
+ providers: [JsonRpcHealthIndicator],
+})
+export class HealthModule {}
diff --git a/packages/data-fetcher/src/health/jsonRpcProvider.health.spec.ts b/packages/data-fetcher/src/health/jsonRpcProvider.health.spec.ts
new file mode 100644
index 0000000000..2cfaa28919
--- /dev/null
+++ b/packages/data-fetcher/src/health/jsonRpcProvider.health.spec.ts
@@ -0,0 +1,56 @@
+import { Test, TestingModule } from "@nestjs/testing";
+import { mock } from "jest-mock-extended";
+import { HealthCheckError } from "@nestjs/terminus";
+import { JsonRpcProviderBase } from "../rpcProvider";
+import { JsonRpcHealthIndicator } from "./jsonRpcProvider.health";
+
+describe("JsonRpcHealthIndicator", () => {
+ const healthIndicatorKey = "rpcProvider";
+ let jsonRpcProviderMock: JsonRpcProviderBase;
+ let jsonRpcHealthIndicator: JsonRpcHealthIndicator;
+
+ beforeEach(async () => {
+ jsonRpcProviderMock = mock();
+
+ const app: TestingModule = await Test.createTestingModule({
+ providers: [
+ JsonRpcHealthIndicator,
+ {
+ provide: JsonRpcProviderBase,
+ useValue: jsonRpcProviderMock,
+ },
+ ],
+ }).compile();
+
+ jsonRpcHealthIndicator = app.get(JsonRpcHealthIndicator);
+ });
+
+ describe("isHealthy", () => {
+ describe("when rpcProvider is open", () => {
+ beforeEach(() => {
+ jest.spyOn(jsonRpcProviderMock, "getState").mockReturnValueOnce("open");
+ });
+
+ it("returns OK health indicator result", async () => {
+ const result = await jsonRpcHealthIndicator.isHealthy(healthIndicatorKey);
+ expect(result).toEqual({ [healthIndicatorKey]: { rpcProviderState: "open", status: "up" } });
+ });
+ });
+
+ describe("when rpcProvider is closed", () => {
+ beforeEach(() => {
+ jest.spyOn(jsonRpcProviderMock, "getState").mockReturnValueOnce("closed");
+ });
+
+ it("throws HealthCheckError error", async () => {
+ expect.assertions(2);
+ try {
+ await jsonRpcHealthIndicator.isHealthy(healthIndicatorKey);
+ } catch (error) {
+ expect(error).toBeInstanceOf(HealthCheckError);
+ expect(error.message).toBe("JSON RPC provider is not in open state");
+ }
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/health/jsonRpcProvider.health.ts b/packages/data-fetcher/src/health/jsonRpcProvider.health.ts
new file mode 100644
index 0000000000..60406bfb4c
--- /dev/null
+++ b/packages/data-fetcher/src/health/jsonRpcProvider.health.ts
@@ -0,0 +1,22 @@
+import { Injectable } from "@nestjs/common";
+import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from "@nestjs/terminus";
+import { JsonRpcProviderBase } from "../rpcProvider";
+
+@Injectable()
+export class JsonRpcHealthIndicator extends HealthIndicator {
+ constructor(private readonly provider: JsonRpcProviderBase) {
+ super();
+ }
+
+ async isHealthy(key: string): Promise {
+ const rpcProviderState = this.provider.getState();
+ const isHealthy = rpcProviderState === "open";
+ const result = this.getStatus(key, isHealthy, { rpcProviderState });
+
+ if (isHealthy) {
+ return result;
+ }
+
+ throw new HealthCheckError("JSON RPC provider is not in open state", result);
+ }
+}
diff --git a/packages/data-fetcher/src/log/index.ts b/packages/data-fetcher/src/log/index.ts
new file mode 100644
index 0000000000..690d9e9229
--- /dev/null
+++ b/packages/data-fetcher/src/log/index.ts
@@ -0,0 +1,2 @@
+export * from "./log.service";
+export * from "./logType";
diff --git a/packages/data-fetcher/src/log/log.service.spec.ts b/packages/data-fetcher/src/log/log.service.spec.ts
new file mode 100644
index 0000000000..90f7d3e3e9
--- /dev/null
+++ b/packages/data-fetcher/src/log/log.service.spec.ts
@@ -0,0 +1,150 @@
+import { Test } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { mock } from "jest-mock-extended";
+import { types } from "zksync-web3";
+import { LogService } from "./log.service";
+import { TransferService } from "../transfer/transfer.service";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { TokenService, Token } from "../token/token.service";
+import { AddressService } from "../address/address.service";
+import { BalanceService } from "../balance/balance.service";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+
+describe("LogService", () => {
+ let logService: LogService;
+ let addressServiceMock: AddressService;
+ let balanceServiceMock: BalanceService;
+ let transferServiceMock: TransferService;
+ let tokenServiceMock: TokenService;
+
+ beforeEach(async () => {
+ addressServiceMock = mock();
+ balanceServiceMock = mock();
+ transferServiceMock = mock();
+ tokenServiceMock = mock();
+
+ const app = await Test.createTestingModule({
+ providers: [
+ LogService,
+ {
+ provide: AddressService,
+ useValue: addressServiceMock,
+ },
+ {
+ provide: BalanceService,
+ useValue: balanceServiceMock,
+ },
+ {
+ provide: TransferService,
+ useValue: transferServiceMock,
+ },
+ {
+ provide: TokenService,
+ useValue: tokenServiceMock,
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ logService = app.get(LogService);
+ });
+
+ describe("getData", () => {
+ const blockDetails = {
+ number: 1,
+ timestamp: new Date().getTime() / 1000,
+ } as types.BlockDetails;
+
+ const deployedContractAddresses = [
+ mock({ address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571" }),
+ mock({ address: "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58" }),
+ ];
+
+ const transfers = [
+ { from: "from1", to: "to1", logIndex: 0 } as Transfer,
+ { from: "from2", to: "to2", logIndex: 1 } as Transfer,
+ ];
+ const logs: types.Log[] = [{ logIndex: 0 } as types.Log, { logIndex: 1 } as types.Log];
+ const tokens: Token[] = [
+ {
+ l1Address: "l1Address1",
+ } as Token,
+ {
+ l1Address: "l1Address2",
+ } as Token,
+ ];
+
+ let transactionReceipt: types.TransactionReceipt;
+ let transactionDetails: types.TransactionDetails;
+
+ beforeEach(() => {
+ jest.spyOn(addressServiceMock, "getContractAddresses").mockResolvedValueOnce(deployedContractAddresses);
+ jest.spyOn(transferServiceMock, "getTransfers").mockReturnValueOnce(transfers);
+ jest.spyOn(tokenServiceMock, "getERC20Token").mockResolvedValueOnce(tokens[0]);
+ jest.spyOn(tokenServiceMock, "getERC20Token").mockResolvedValueOnce(tokens[1]);
+
+ transactionReceipt = mock();
+ transactionDetails = mock({
+ receivedAt: new Date(),
+ });
+ });
+
+ describe("when transaction details and receipt are defined", () => {
+ beforeEach(() => {
+ transactionReceipt = mock({
+ transactionIndex: 0,
+ logs: logs,
+ });
+ });
+
+ it("returns data with transaction transfers", async () => {
+ const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledTimes(1);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledWith(
+ logs,
+ blockDetails,
+ transactionDetails,
+ transactionReceipt
+ );
+ expect(logsData.transfers).toEqual(transfers);
+ });
+
+ it("tracks changed balances", async () => {
+ await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledWith(transfers);
+ });
+
+ it("returns data with deployed contracts' addresses", async () => {
+ const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(addressServiceMock.getContractAddresses).toHaveBeenCalledTimes(1);
+ expect(addressServiceMock.getContractAddresses).toHaveBeenCalledWith(logs, transactionReceipt);
+ expect(logsData.contractAddresses).toEqual(deployedContractAddresses);
+ });
+
+ it("returns data with ERC20 tokens", async () => {
+ const logsData = await logService.getData(logs, blockDetails, transactionDetails, transactionReceipt);
+ expect(tokenServiceMock.getERC20Token).toHaveBeenCalledTimes(2);
+ expect(tokenServiceMock.getERC20Token).toHaveBeenCalledWith(deployedContractAddresses[0], transactionReceipt);
+ expect(tokenServiceMock.getERC20Token).toHaveBeenCalledWith(deployedContractAddresses[1], transactionReceipt);
+ expect(logsData.tokens).toEqual(tokens);
+ });
+ });
+
+ describe("when transaction details and receipt are not defined", () => {
+ it("tracks changed balances", async () => {
+ await logService.getData(logs, blockDetails);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledTimes(1);
+ expect(balanceServiceMock.trackChangedBalances).toHaveBeenCalledWith(transfers);
+ });
+
+ it("returns data with transaction transfers", async () => {
+ const logsData = await logService.getData(logs, blockDetails);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledTimes(1);
+ expect(transferServiceMock.getTransfers).toHaveBeenCalledWith(logs, blockDetails, undefined, undefined);
+ expect(logsData.transfers).toEqual(transfers);
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/log/log.service.ts b/packages/data-fetcher/src/log/log.service.ts
new file mode 100644
index 0000000000..18ce191b22
--- /dev/null
+++ b/packages/data-fetcher/src/log/log.service.ts
@@ -0,0 +1,69 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { types } from "zksync-web3";
+import { AddressService } from "../address/address.service";
+import { BalanceService } from "../balance/balance.service";
+import { TransferService } from "../transfer/transfer.service";
+import { TokenService } from "../token/token.service";
+import { Transfer } from "../transfer/interfaces/transfer.interface";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+import { Token } from "../token/token.service";
+
+export interface LogsData {
+ transfers: Transfer[];
+ contractAddresses?: ContractAddress[];
+ tokens?: Token[];
+}
+
+@Injectable()
+export class LogService {
+ private readonly logger: Logger;
+
+ public constructor(
+ private readonly addressService: AddressService,
+ private readonly balanceService: BalanceService,
+ private readonly transferService: TransferService,
+ private readonly tokenService: TokenService
+ ) {
+ this.logger = new Logger(LogService.name);
+ }
+
+ public async getData(
+ logs: types.Log[],
+ blockDetails: types.BlockDetails,
+ transactionDetails?: types.TransactionDetails,
+ transactionReceipt?: types.TransactionReceipt
+ ): Promise {
+ const transfers = this.transferService.getTransfers(logs, blockDetails, transactionDetails, transactionReceipt);
+
+ const logsData: LogsData = {
+ transfers,
+ };
+
+ this.balanceService.trackChangedBalances(transfers);
+
+ if (transactionReceipt) {
+ const transactionHash = transactionReceipt.transactionHash;
+
+ this.logger.debug({ message: "Extracting contracts", blockNumber: blockDetails.number, transactionHash });
+ const contractAddresses = await this.addressService.getContractAddresses(logs, transactionReceipt);
+
+ this.logger.debug({
+ message: "Extracting ERC20 tokens",
+ blockNumber: blockDetails.number,
+ transactionHash,
+ });
+ const tokens = (
+ await Promise.all(
+ contractAddresses.map((contractAddress) =>
+ this.tokenService.getERC20Token(contractAddress, transactionReceipt)
+ )
+ )
+ ).filter((token) => !!token);
+
+ logsData.contractAddresses = contractAddresses;
+ logsData.tokens = tokens;
+ }
+
+ return logsData;
+ }
+}
diff --git a/packages/data-fetcher/src/log/logType.spec.ts b/packages/data-fetcher/src/log/logType.spec.ts
new file mode 100644
index 0000000000..237834f4a2
--- /dev/null
+++ b/packages/data-fetcher/src/log/logType.spec.ts
@@ -0,0 +1,22 @@
+import { types } from "zksync-web3";
+import { mock } from "jest-mock-extended";
+import { isLogOfType, LogType } from "./logType";
+
+describe("isLogOfType", () => {
+ let log: types.Log;
+ beforeEach(() => {
+ log = mock({
+ topics: ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "arg1"],
+ });
+ });
+
+ it("returns true if the first log topic is equal to any of the specified event hashes", () => {
+ const result = isLogOfType(log, [LogType.Approval, LogType.Transfer, LogType.BridgeBurn]);
+ expect(result).toBe(true);
+ });
+
+ it("returns false if the first log topic isn't equal to any of the specified event hashes", () => {
+ const result = isLogOfType(log, [LogType.BridgeBurn, LogType.Approval, LogType.BridgeInitialization]);
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/data-fetcher/src/log/logType.ts b/packages/data-fetcher/src/log/logType.ts
new file mode 100644
index 0000000000..7a6dcb9a7d
--- /dev/null
+++ b/packages/data-fetcher/src/log/logType.ts
@@ -0,0 +1,54 @@
+import { types } from "zksync-web3";
+
+export enum LogType {
+ // ERC20
+ // event Transfer(address indexed from, address indexed to, uint256 value);
+ // ERC721
+ // event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
+ Transfer = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+
+ // event Approval(address indexed owner, address indexed spender, uint256 value);
+ Approval = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+
+ //
+ // BridgeInitialization was used first, then it was renamed to BridgeInitialize so now we should support both
+ // event BridgeInitialization(address indexed l1Token, string name, string symbol, uint8 decimals);
+ BridgeInitialization = "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c",
+ // event BridgeInitialize(address indexed l1Token, string name, string symbol, uint8 decimals);
+ BridgeInitialize = "0x81e8e92e5873539605a102eddae7ed06d19bea042099a437cbc3644415eb7404",
+ //
+
+ // event BridgeMint(address indexed _account, uint256 _amount);
+ BridgeMint = "0x397b33b307fc137878ebfc75b295289ec0ee25a31bb5bf034f33256fe8ea2aa6",
+
+ // event BridgeBurn(address indexed _account, uint256 _amount);
+ BridgeBurn = "0x9b5b9a05e4726d8bb959f1440e05c6b8109443f2083bc4e386237d7654526553",
+
+ // event DepositInitiated(address indexed from, address indexed to, address indexed l1Token, uint256 amount);
+ DepositInitiated = "0x7abe8fd2d210cf1e5d2cb3e277afd776d77269c8869b02c39f0bb542de0fdba1",
+
+ // event FinalizeDeposit(address indexed l1Sender, address indexed l2Receiver, address indexed l2Token, uint256 amount);
+ FinalizeDeposit = "0xb84fba9af218da60d299dc177abd5805e7ac541d2673cbee7808c10017874f63",
+
+ // event ClaimedFailedDeposit(address indexed to, address indexed l1Token, uint256 amount);
+ ClaimedFailedDeposit = "0xbe066dc591f4a444f75176d387c3e6c775e5706d9ea9a91d11eb49030c66cf60",
+
+ // event WithdrawalInitiated(address indexed l2Sender, address indexed l1Receiver, address indexed l2Token, uint256 amount);
+ WithdrawalInitiated = "0x2fc3848834aac8e883a2d2a17a7514dc4f2d3dd268089df9b9f5d918259ef3b0",
+
+ // event WithdrawalFinalized(address indexed to, address indexed l1Token, uint256 amount);
+ WithdrawalFinalized = "0xac1b18083978656d557d6e91c88203585cfda1031bdb14538327121ef140d383",
+
+ // event ContractDeployed(address indexed deployerAddress, bytes32 indexed bytecodeHash, address indexed contractAddress);
+ ContractDeployed = "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+
+ // event Mint(address indexed account, uint256 amount)
+ Mint = "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+
+ // event Withdrawal(address indexed _l2Sender, address indexed _l1Receiver, uint256 _amount)
+ Withdrawal = "0x2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b6398",
+}
+
+export const isLogOfType = (log: types.Log, types: LogType[]): boolean => {
+ return types.some((type) => log.topics[0] === type);
+};
diff --git a/packages/data-fetcher/src/logger.ts b/packages/data-fetcher/src/logger.ts
new file mode 100644
index 0000000000..48476324d8
--- /dev/null
+++ b/packages/data-fetcher/src/logger.ts
@@ -0,0 +1,27 @@
+import { utilities, WinstonModule } from "nest-winston";
+import { format, transports, Logform } from "winston";
+
+const { NODE_ENV, LOG_LEVEL } = process.env;
+
+let defaultLogLevel = "debug";
+const loggerFormatters: Logform.Format[] = [
+ format.timestamp({
+ format: "DD/MM/YYYY HH:mm:ss.SSS",
+ }),
+ format.ms(),
+ utilities.format.nestLike("Worker", {}),
+];
+
+if (NODE_ENV === "production") {
+ defaultLogLevel = "info";
+ loggerFormatters.push(format.json());
+}
+
+export default WinstonModule.createLogger({
+ level: LOG_LEVEL || defaultLogLevel,
+ transports: [
+ new transports.Console({
+ format: format.combine(...loggerFormatters),
+ }),
+ ],
+});
diff --git a/packages/data-fetcher/src/main.ts b/packages/data-fetcher/src/main.ts
new file mode 100644
index 0000000000..dfcdfc019d
--- /dev/null
+++ b/packages/data-fetcher/src/main.ts
@@ -0,0 +1,23 @@
+import { NestFactory } from "@nestjs/core";
+import { ConfigService } from "@nestjs/config";
+import logger from "./logger";
+import { AppModule } from "./app.module";
+import { ResponseTransformInterceptor } from "./common/interceptors/responseTransform.interceptor";
+
+async function bootstrap() {
+ process.on("uncaughtException", function (error) {
+ logger.error(error.message, error.stack, "UnhandledExceptions");
+ process.exit(1);
+ });
+
+ const app = await NestFactory.create(AppModule, {
+ logger,
+ });
+
+ const configService = app.get(ConfigService);
+ app.enableShutdownHooks();
+ app.useGlobalInterceptors(new ResponseTransformInterceptor());
+ await app.listen(configService.get("port"));
+}
+
+bootstrap();
diff --git a/packages/data-fetcher/src/metrics/index.ts b/packages/data-fetcher/src/metrics/index.ts
new file mode 100644
index 0000000000..99e2d7f1e6
--- /dev/null
+++ b/packages/data-fetcher/src/metrics/index.ts
@@ -0,0 +1,2 @@
+export * from "./metrics.provider";
+export * from "./metrics.module";
diff --git a/packages/data-fetcher/src/metrics/metrics.module.ts b/packages/data-fetcher/src/metrics/metrics.module.ts
new file mode 100644
index 0000000000..bb70e8f94d
--- /dev/null
+++ b/packages/data-fetcher/src/metrics/metrics.module.ts
@@ -0,0 +1,8 @@
+import { Module } from "@nestjs/common";
+import { metricProviders } from "./metrics.provider";
+
+@Module({
+ providers: metricProviders,
+ exports: metricProviders,
+})
+export class MetricsModule {}
diff --git a/packages/data-fetcher/src/metrics/metrics.provider.ts b/packages/data-fetcher/src/metrics/metrics.provider.ts
new file mode 100644
index 0000000000..1f7b801d7a
--- /dev/null
+++ b/packages/data-fetcher/src/metrics/metrics.provider.ts
@@ -0,0 +1,63 @@
+import { Provider } from "@nestjs/common";
+import { makeHistogramProvider } from "@willsoto/nestjs-prometheus";
+
+export const BLOCK_PROCESSING_DURATION_METRIC_NAME = "block_processing_duration_seconds";
+export type BlockProcessingMetricLabels = "status" | "action";
+
+export const TRANSACTION_PROCESSING_DURATION_METRIC_NAME = "transaction_processing_duration_seconds";
+export type ProcessingActionMetricLabel = "action";
+
+export const BALANCES_PROCESSING_DURATION_METRIC_NAME = "balances_processing_duration_seconds";
+
+export const GET_BLOCK_INFO_DURATION_METRIC_NAME = "get_block_info_duration_seconds";
+export const GET_TRANSACTION_INFO_DURATION_METRIC_NAME = "get_transaction_info_duration_seconds";
+export const GET_TOKEN_INFO_DURATION_METRIC_NAME = "get_token_info_duration_seconds";
+
+export const BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME = "blockchain_rpc_call_duration_seconds";
+export type BlockchainRpcCallMetricLabel = "function";
+
+const metricsBuckets = [
+ 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.25, 1.5, 2, 2.5, 3, 4,
+ 5, 7, 10, 20, 30,
+];
+
+export const metricProviders: Provider[] = [
+ makeHistogramProvider({
+ name: BLOCK_PROCESSING_DURATION_METRIC_NAME,
+ help: "block processing duration in seconds.",
+ labelNames: ["status", "action"],
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: TRANSACTION_PROCESSING_DURATION_METRIC_NAME,
+ help: "transaction processing duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: BALANCES_PROCESSING_DURATION_METRIC_NAME,
+ help: "balances processing duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: BLOCKCHAIN_RPC_CALL_DURATION_METRIC_NAME,
+ help: "blockchain rpc call duration in seconds.",
+ labelNames: ["function"],
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: GET_BLOCK_INFO_DURATION_METRIC_NAME,
+ help: "get block info duration in seconds.",
+ labelNames: ["action"],
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: GET_TRANSACTION_INFO_DURATION_METRIC_NAME,
+ help: "get transaction info duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+ makeHistogramProvider({
+ name: GET_TOKEN_INFO_DURATION_METRIC_NAME,
+ help: "get token info duration in seconds.",
+ buckets: metricsBuckets,
+ }),
+];
diff --git a/packages/data-fetcher/src/rpcProvider/index.ts b/packages/data-fetcher/src/rpcProvider/index.ts
new file mode 100644
index 0000000000..f00c9c8b85
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/index.ts
@@ -0,0 +1,4 @@
+export * from "./jsonRpcProviderBase";
+export * from "./jsonRpcProviderExtended";
+export * from "./webSocketProviderExtended";
+export * from "./wrappedWebSocketProvider";
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProvider.module.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProvider.module.ts
new file mode 100644
index 0000000000..e3db352c58
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProvider.module.ts
@@ -0,0 +1,57 @@
+import { Module, DynamicModule, Logger } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { JsonRpcProviderBase, JsonRpcProviderExtended, WrappedWebSocketProvider } from "./index";
+
+@Module({
+ providers: [
+ Logger,
+ {
+ provide: JsonRpcProviderBase,
+ useFactory: (configService: ConfigService, logger: Logger) => {
+ const providerUrl = configService.get("blockchain.rpcUrl");
+ const connectionTimeout = configService.get("blockchain.rpcCallConnectionTimeout");
+ const connectionQuickTimeout = configService.get("blockchain.rpcCallConnectionQuickTimeout");
+ const providerUrlProtocol = new URL(providerUrl).protocol;
+
+ logger.debug(`Initializing RPC provider with the following URL: ${providerUrl}.`, "RpcProviderModule");
+
+ if (providerUrlProtocol === "http:" || providerUrlProtocol === "https:") {
+ return new JsonRpcProviderExtended(providerUrl, connectionTimeout, connectionQuickTimeout);
+ }
+
+ throw new Error(
+ `RPC URL protocol is not supported. HTTP(s) URL is expected. Actual protocol: ${providerUrlProtocol}.`
+ );
+ },
+ inject: [ConfigService, Logger],
+ },
+ {
+ provide: WrappedWebSocketProvider,
+ useFactory: (configService: ConfigService, logger: Logger) => {
+ const providerUrl = configService.get("blockchain.wsRpcUrl");
+ const connectionTimeout = configService.get("blockchain.rpcCallConnectionTimeout");
+ const connectionQuickTimeout = configService.get("blockchain.rpcCallConnectionQuickTimeout");
+ const maxConnections = configService.get("blockchain.wsMaxConnections");
+ const useWebSocketsForTransactions = configService.get("blockchain.useWebSocketsForTransactions");
+
+ if (!useWebSocketsForTransactions) {
+ return null;
+ }
+
+ logger.debug(`Initializing WS RPC provider with the following URL: ${providerUrl}.`, "RpcProviderModule");
+
+ return new WrappedWebSocketProvider(providerUrl, connectionTimeout, connectionQuickTimeout, maxConnections);
+ },
+ inject: [ConfigService, Logger],
+ },
+ ],
+ exports: [JsonRpcProviderBase, WrappedWebSocketProvider],
+})
+export class JsonRpcProviderModule {
+ static forRoot(): DynamicModule {
+ return {
+ module: JsonRpcProviderModule,
+ global: true,
+ };
+ }
+}
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProviderBase.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderBase.ts
new file mode 100644
index 0000000000..d88b7c11fd
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderBase.ts
@@ -0,0 +1,7 @@
+import { Provider } from "zksync-web3";
+
+export type ProviderState = "connecting" | "open" | "closed";
+
+export abstract class JsonRpcProviderBase extends Provider {
+ public abstract getState(): ProviderState;
+}
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.spec.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.spec.ts
new file mode 100644
index 0000000000..5d66e4526e
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.spec.ts
@@ -0,0 +1,119 @@
+import { mock } from "jest-mock-extended";
+const baseSendPromise = jest.fn();
+class JsonRpcProviderBaseMock {
+ public send() {
+ return baseSendPromise();
+ }
+}
+jest.mock("../logger");
+jest.useFakeTimers();
+jest.mock("zksync-web3", () => ({
+ Provider: JsonRpcProviderBaseMock,
+}));
+import { JsonRpcProviderExtended } from "./jsonRpcProviderExtended";
+
+describe("JsonRpcProviderExtended", () => {
+ let jsonRpcProvider: JsonRpcProviderExtended;
+ const timer = mock();
+ let lastCallback: () => void;
+
+ beforeEach(async () => {
+ jsonRpcProvider = new JsonRpcProviderExtended("url", 120_000, 10_000);
+
+ jest.spyOn(global, "setTimeout").mockImplementation((callback: () => void) => {
+ lastCallback = callback;
+ return timer;
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("send", () => {
+ it("calls base implementation and returns its value", async () => {
+ baseSendPromise.mockResolvedValueOnce({
+ method: "method",
+ params: [1, 2],
+ });
+ const result = await jsonRpcProvider.send("method", [1, 2]);
+ expect(result).toStrictEqual({
+ method: "method",
+ params: [1, 2],
+ });
+ });
+
+ it("starts quick timeout", async () => {
+ await jsonRpcProvider.send("method", [1, 2]);
+ expect(global.setTimeout).toBeCalledTimes(1);
+ expect(global.setTimeout).toBeCalledWith(expect.any(Function), 10_000);
+ });
+
+ it("clears quick timeout", async () => {
+ jest.spyOn(global, "clearTimeout");
+ await jsonRpcProvider.send("method", [1, 2]);
+ expect(global.clearTimeout).toBeCalledTimes(1);
+ expect(global.clearTimeout).toBeCalledWith(timer);
+ });
+
+ describe("when base send throws an error", () => {
+ const error = new Error("test error");
+ beforeEach(() => {
+ baseSendPromise.mockRejectedValue(error);
+ });
+
+ it("throws the same error", async () => {
+ await expect(jsonRpcProvider.send("method", [1, 2])).rejects.toThrowError(error);
+ });
+ });
+
+ describe("when timeout occurs faster than send returns value", () => {
+ beforeEach(() => {
+ baseSendPromise.mockImplementationOnce(() => {
+ lastCallback();
+ return {
+ method: "method1",
+ params: [1, 2],
+ };
+ });
+ baseSendPromise.mockResolvedValueOnce({
+ method: "method2",
+ params: [2, 3],
+ });
+ });
+
+ it("waits for internal timeout and returns the result of the second call", async () => {
+ const result = await jsonRpcProvider.send("method", [1, 2]);
+ expect(global.setTimeout).toBeCalledTimes(1);
+ expect(global.setTimeout).toBeCalledWith(expect.any(Function), 10_000);
+ expect(result).toStrictEqual({
+ method: "method2",
+ params: [2, 3],
+ });
+ });
+
+ it("when timeout is already cleared does not throw an error", async () => {
+ baseSendPromise.mockImplementationOnce(() => {
+ lastCallback();
+ return {
+ method: "method1",
+ params: [1, 2],
+ };
+ });
+ jest.spyOn(global, "setTimeout").mockImplementation((callback: () => void) => {
+ lastCallback = callback;
+ return null;
+ });
+ const result = await jsonRpcProvider.send("method", [1, 2]);
+ expect(result).toBeUndefined();
+ });
+ });
+ });
+
+ describe("getState", () => {
+ it("returns open", () => {
+ const state = jsonRpcProvider.getState();
+ expect(state).toBe("open");
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.ts b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.ts
new file mode 100644
index 0000000000..eb2caaaf30
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/jsonRpcProviderExtended.ts
@@ -0,0 +1,64 @@
+import { Provider } from "zksync-web3";
+import { ProviderState, JsonRpcProviderBase } from "./jsonRpcProviderBase";
+import logger from "../logger";
+
+export class QuickTimeoutError extends Error {
+ constructor() {
+ super();
+ }
+}
+
+export class JsonRpcProviderExtended extends Provider implements JsonRpcProviderBase {
+ private readonly connectionQuickTimeout;
+ constructor(providerUrl: string, connectionTimeout: number, connectionQuickTimeout: number) {
+ super({
+ url: providerUrl,
+ timeout: connectionTimeout,
+ });
+ this.connectionQuickTimeout = connectionQuickTimeout;
+ }
+
+ private startQuickTimeout(timeout) {
+ let timer: NodeJS.Timer = null;
+ const promise = new Promise((resolve, reject) => {
+ timer = setTimeout(() => {
+ timer ? reject(new QuickTimeoutError()) : resolve(undefined);
+ }, timeout);
+ });
+
+ const cancel = () => {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ return { promise, cancel };
+ }
+
+ public getState(): ProviderState {
+ return "open";
+ }
+
+ public override async send(method: string, params: Array): Promise {
+ const quickTimeout = this.startQuickTimeout(this.connectionQuickTimeout);
+ try {
+ return await Promise.race([quickTimeout.promise, super.send(method, params)]);
+ } catch (e) {
+ if (e instanceof QuickTimeoutError) {
+ logger.error({
+ message: "RPC provider: quick timeout",
+ stack: e.stack,
+ method,
+ params,
+ timeout: this.connectionQuickTimeout,
+ context: JsonRpcProviderExtended.name,
+ });
+ return super.send(method, params);
+ }
+ throw e;
+ } finally {
+ quickTimeout.cancel();
+ }
+ }
+}
diff --git a/packages/data-fetcher/src/rpcProvider/webSocketProviderExtended.ts b/packages/data-fetcher/src/rpcProvider/webSocketProviderExtended.ts
new file mode 100644
index 0000000000..0d777bc4b7
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/webSocketProviderExtended.ts
@@ -0,0 +1,120 @@
+import { providers } from "ethers";
+import logger from "../logger";
+import { ProviderState } from "./jsonRpcProviderBase";
+
+const expectedPongBack = 10000;
+const checkInterval = 12000;
+const pendingRequestsLimit = 100000;
+
+export class TimeoutError extends Error {
+ constructor(message: string) {
+ super(message);
+ }
+}
+
+export class WebSocketProviderExtended extends providers.WebSocketProvider {
+ private state: ProviderState = "connecting";
+ private readonly connectionQuickTimeout: number;
+ private readonly connectionTimeout: number;
+
+ constructor(providerUrl, connectionTimeout: number, connectionQuickTimeout: number) {
+ super(providerUrl);
+ this.connectionTimeout = connectionTimeout;
+ this.connectionQuickTimeout = connectionQuickTimeout;
+ this.attachStateCheck();
+ }
+
+ public override async send(method: string, params: Array): Promise {
+ const quickTimeout = this.startTimeout(this.connectionQuickTimeout, "WS RPC provider: quick timeout");
+ try {
+ return await Promise.race([quickTimeout.promise, super.send(method, params)]);
+ } catch (e) {
+ if (e instanceof TimeoutError) {
+ logger.error({
+ message: e.message,
+ stack: e.stack,
+ method,
+ params,
+ timeout: this.connectionQuickTimeout,
+ context: WebSocketProviderExtended.name,
+ });
+
+ const timeout = this.startTimeout(this.connectionTimeout, "WS RPC provider: timeout");
+ try {
+ return await Promise.race([timeout.promise, super.send(method, params)]);
+ } finally {
+ timeout.cancel();
+ }
+ }
+ throw e;
+ } finally {
+ quickTimeout.cancel();
+ }
+ }
+
+ private startTimeout(timeout: number, errorMessage = "WS RPC provider: timeout") {
+ let timer: NodeJS.Timer = null;
+ const promise = new Promise((resolve, reject) => {
+ timer = setTimeout(() => {
+ timer ? reject(new TimeoutError(errorMessage)) : resolve(undefined);
+ }, timeout);
+ });
+
+ const cancel = () => {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ return { promise, cancel };
+ }
+
+ private attachStateCheck(): void {
+ let pingTimeout: NodeJS.Timeout;
+ let keepAliveInterval: NodeJS.Timeout;
+
+ this._websocket.on("open", () => {
+ this.state = "open";
+
+ logger.debug("Web socket has been opened");
+
+ keepAliveInterval = setInterval(() => {
+ this._websocket.ping();
+ pingTimeout = setTimeout(() => {
+ logger.error({
+ message: "No response for the ping request. Web socket connection will be terminated",
+ context: WebSocketProviderExtended.name,
+ });
+ this._websocket.terminate();
+ }, expectedPongBack);
+
+ if (Object.keys(this._requests).length > pendingRequestsLimit) {
+ logger.error({
+ message: "Too many pending requests. Web socket connection will be terminated",
+ context: WebSocketProviderExtended.name,
+ });
+ this._websocket.terminate();
+ return;
+ }
+ }, checkInterval);
+ });
+
+ this._websocket.on("close", () => {
+ this.state = "closed";
+
+ logger.debug("Web socket has been closed");
+
+ if (keepAliveInterval) clearInterval(keepAliveInterval);
+ if (pingTimeout) clearTimeout(pingTimeout);
+ });
+
+ this._websocket.on("pong", () => {
+ if (pingTimeout) clearTimeout(pingTimeout);
+ });
+ }
+
+ public getState(): ProviderState {
+ return this.state;
+ }
+}
diff --git a/packages/data-fetcher/src/rpcProvider/wrappedWebSocketProvider.ts b/packages/data-fetcher/src/rpcProvider/wrappedWebSocketProvider.ts
new file mode 100644
index 0000000000..d9d34e6db4
--- /dev/null
+++ b/packages/data-fetcher/src/rpcProvider/wrappedWebSocketProvider.ts
@@ -0,0 +1,56 @@
+import { ProviderState } from "./jsonRpcProviderBase";
+import { WebSocketProviderExtended } from "./webSocketProviderExtended";
+
+const monitorInterval = 10000;
+
+export class WrappedWebSocketProvider {
+ private readonly providerUrl: string;
+ private readonly connectionTimeout: number;
+ private readonly connectionQuickTimeout: number;
+ private instances: WebSocketProviderExtended[] = [];
+
+ constructor(providerUrl: string, connectionTimeout: number, connectionQuickTimeout: number, maxConnections = 5) {
+ this.providerUrl = providerUrl;
+ this.connectionTimeout = connectionTimeout;
+ this.connectionQuickTimeout = connectionQuickTimeout;
+
+ for (let i = 0; i < maxConnections; i++) {
+ this.instances[i] = new WebSocketProviderExtended(
+ this.providerUrl,
+ this.connectionTimeout,
+ this.connectionQuickTimeout
+ );
+ }
+ this.monitorInstances();
+ }
+
+ public getProvider(): WebSocketProviderExtended {
+ const totalActiveInstances = this.instances.filter((instance) => instance.getState() !== "closed");
+ const randomInstanceNumber = Math.floor(Math.random() * totalActiveInstances.length);
+ return this.instances[randomInstanceNumber];
+ }
+
+ private monitorInstances(): void {
+ setInterval(() => {
+ for (let i = 0; i < this.instances.length; i++) {
+ if (this.instances[i].getState() === "closed") {
+ this.instances[i] = new WebSocketProviderExtended(
+ this.providerUrl,
+ this.connectionTimeout,
+ this.connectionQuickTimeout
+ );
+ }
+ }
+ }, monitorInterval);
+ }
+
+ public getState(): ProviderState {
+ if (this.instances.find((instance) => instance.getState() === "open")) {
+ return "open";
+ }
+ if (this.instances.find((instance) => instance.getState() === "connecting")) {
+ return "connecting";
+ }
+ return "closed";
+ }
+}
diff --git a/packages/data-fetcher/src/token/token.service.spec.ts b/packages/data-fetcher/src/token/token.service.spec.ts
new file mode 100644
index 0000000000..499c04b606
--- /dev/null
+++ b/packages/data-fetcher/src/token/token.service.spec.ts
@@ -0,0 +1,468 @@
+import { mock } from "jest-mock-extended";
+import { types, utils } from "zksync-web3";
+import { Test, TestingModule } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TokenService } from "./token.service";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+
+describe("TokenService", () => {
+ let tokenService: TokenService;
+ let blockchainServiceMock: BlockchainService;
+ let startGetTokenInfoDurationMetricMock: jest.Mock;
+ let stopGetTokenInfoDurationMetricMock: jest.Mock;
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock({
+ bridgeAddresses: {
+ l2Erc20DefaultBridge: "0x0000000000000000000000000000000000001111",
+ },
+ });
+
+ stopGetTokenInfoDurationMetricMock = jest.fn();
+ startGetTokenInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetTokenInfoDurationMetricMock);
+
+ const app: TestingModule = await Test.createTestingModule({
+ providers: [
+ TokenService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ {
+ provide: "PROM_METRIC_GET_TOKEN_INFO_DURATION_SECONDS",
+ useValue: {
+ startTimer: startGetTokenInfoDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ tokenService = app.get(TokenService);
+ });
+
+ describe("getERC20Token", () => {
+ let deployedContractAddress: ContractAddress;
+ let transactionReceipt: types.TransactionReceipt;
+ let tokenData;
+
+ beforeEach(() => {
+ tokenData = {
+ symbol: "symbol",
+ decimals: 18,
+ name: "name",
+ };
+
+ transactionReceipt = mock({
+ logs: [],
+ to: "0x0000000000000000000000000000000000001111",
+ });
+
+ deployedContractAddress = mock({
+ address: "0xdc187378edd8ed1585fb47549cc5fe633295d571",
+ blockNumber: 10,
+ transactionHash: "transactionHash",
+ logIndex: 20,
+ });
+
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValue(tokenData);
+ });
+
+ describe("when there is neither bridge initialization nor bridge initialize log for the current token address", () => {
+ beforeEach(() => {
+ transactionReceipt.logs = [];
+ });
+
+ it("starts the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+
+ describe("when contract is ETH L2 contract", () => {
+ it("returns ETH token with ETH l1Address", async () => {
+ const ethTokenData = {
+ symbol: "ETH",
+ decimals: 18,
+ name: "Ethers",
+ };
+ const deployedETHContractAddress = mock({
+ address: utils.L2_ETH_TOKEN_ADDRESS,
+ blockNumber: 0,
+ transactionHash: "transactionHash",
+ logIndex: 0,
+ });
+ (blockchainServiceMock.getERC20TokenData as jest.Mock).mockResolvedValueOnce(ethTokenData);
+
+ const token = await tokenService.getERC20Token(deployedETHContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...ethTokenData,
+ blockNumber: deployedETHContractAddress.blockNumber,
+ transactionHash: deployedETHContractAddress.transactionHash,
+ l2Address: deployedETHContractAddress.address,
+ l1Address: utils.ETH_ADDRESS,
+ logIndex: deployedETHContractAddress.logIndex,
+ });
+ });
+ });
+
+ it("tracks the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ describe("if ERC20 Contract function throws an exception", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockImplementation(() => {
+ throw new Error("Ethers Contract error");
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+
+ it("does not track the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+
+ describe("when transaction receipt does not contain logs", () => {
+ beforeEach(() => {
+ transactionReceipt.logs = null;
+ });
+
+ it("starts the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+
+ it("tracks the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ describe("if ERC20 Contract function throws an exception", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockImplementation(() => {
+ throw new Error("Ethers Contract error");
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+
+ it("does not track the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(stopGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+
+ describe("when there is a bridge initialization log in transaction receipt for the current token address", () => {
+ let bridgedToken;
+
+ beforeEach(() => {
+ transactionReceipt.logs = [
+ mock({
+ topics: [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "0x0100014340e955cbf39159da998b3374bee8f3c0b3c75a7a9e3df6b85052379d",
+ "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571",
+ ],
+ }),
+ mock({
+ address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ topics: [
+ "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c",
+ "0x0000000000000000000000000db321efaa9e380d0b37b55b530cdaa62728b9a3",
+ ],
+ data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000",
+ }),
+ ];
+
+ bridgedToken = {
+ name: "ADL1",
+ symbol: "ADL1",
+ decimals: 18,
+ };
+ });
+
+ it("extract token info from log and does not call web3 API to get token data", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(0);
+ });
+
+ it("returns the token with l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...bridgedToken,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ l1Address: "0x0Db321EFaa9E380d0B37B55B530CDaA62728B9a3",
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("when there is a bridge initialization log in transaction receipt which is not produced by the bridge contract", () => {
+ beforeEach(() => {
+ transactionReceipt.to = "0x0000000000000000000000000000000000001112";
+ transactionReceipt.logs = [
+ mock({
+ topics: [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "0x0100014340e955cbf39159da998b3374bee8f3c0b3c75a7a9e3df6b85052379d",
+ "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571",
+ ],
+ }),
+ mock({
+ address: "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ topics: [
+ "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c",
+ "0x0000000000000000000000000db321efaa9e380d0b37b55b530cdaa62728b9a3",
+ ],
+ data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000",
+ }),
+ ];
+ });
+
+ it("starts the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("when there is a bridge initialize log in transaction receipt for the current token address", () => {
+ let bridgedToken;
+
+ beforeEach(() => {
+ transactionReceipt.logs = [
+ mock({
+ topics: [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000913389f49358cb49a8e9e984a5871df43f80eb96",
+ "0x01000125c745537b5254be2ca086aee7fbd5d91789ed15790a942f9422d36447",
+ "0x0000000000000000000000005a393c95e7bddd0281650023d8c746fb1f596b7b",
+ ],
+ }),
+ mock({
+ address: "0x5a393c95e7Bddd0281650023D8C746fB1F596B7b",
+ topics: [
+ "0x81e8e92e5873539605a102eddae7ed06d19bea042099a437cbc3644415eb7404",
+ "0x000000000000000000000000c8f8ce6491227a6a2ab92e67a64011a4eba1c6cf",
+ ],
+ data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000134c313131206465706c6f79656420746f204c310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c31313100000000000000000000000000000000000000000000000000000000",
+ }),
+ ];
+
+ deployedContractAddress = mock({
+ address: "0x5a393c95e7bddd0281650023d8c746fb1f596b7b",
+ blockNumber: 10,
+ transactionHash: "transactionHash",
+ logIndex: 20,
+ });
+
+ bridgedToken = {
+ name: "L111 deployed to L1",
+ symbol: "L111",
+ decimals: 18,
+ };
+ });
+
+ it("extract token info from log and does not call web3 API to get token data", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(0);
+ });
+
+ it("returns the token with l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...bridgedToken,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ l1Address: "0xc8F8cE6491227a6a2Ab92e67a64011a4Eba1C6CF",
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("when there is a bridge initialize log in transaction receipt which is not produced by the bridge contract", () => {
+ beforeEach(() => {
+ transactionReceipt.to = "0x0000000000000000000000000000000000001112";
+ transactionReceipt.logs = [
+ mock({
+ topics: [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000913389f49358cb49a8e9e984a5871df43f80eb96",
+ "0x01000125c745537b5254be2ca086aee7fbd5d91789ed15790a942f9422d36447",
+ "0x0000000000000000000000005a393c95e7bddd0281650023d8c746fb1f596b7b",
+ ],
+ }),
+ mock({
+ address: "0x5a393c95e7Bddd0281650023D8C746fB1F596B7b",
+ topics: [
+ "0x81e8e92e5873539605a102eddae7ed06d19bea042099a437cbc3644415eb7404",
+ "0x000000000000000000000000c8f8ce6491227a6a2ab92e67a64011a4eba1c6cf",
+ ],
+ data: "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000134c313131206465706c6f79656420746f204c310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c31313100000000000000000000000000000000000000000000000000000000",
+ }),
+ ];
+
+ deployedContractAddress = mock({
+ address: "0x5a393c95e7bddd0281650023d8c746fb1f596b7b",
+ blockNumber: 10,
+ transactionHash: "transactionHash",
+ logIndex: 20,
+ });
+ });
+
+ it("starts the get token info duration metric", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(startGetTokenInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("gets token data by the contract address", async () => {
+ await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getERC20TokenData).toHaveBeenCalledWith(deployedContractAddress.address);
+ });
+
+ it("returns the token without l1Address", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+
+ describe("if the token symbol or name has special symbols", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({
+ ...tokenData,
+ symbol: "\0\0\0\0\0\0test symbol",
+ name: "\0\0\0\0\0\0test name",
+ });
+ });
+
+ it("returns token with special chars replaced", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toEqual({
+ blockNumber: 10,
+ decimals: 18,
+ l2Address: "0xdc187378edd8ed1585fb47549cc5fe633295d571",
+ logIndex: 20,
+ name: "test name",
+ symbol: "test symbol",
+ transactionHash: "transactionHash",
+ });
+ });
+ });
+
+ describe("if the token symbol is empty", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({
+ ...tokenData,
+ symbol: "",
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+ });
+
+ describe("if the token symbol has special symbols only", () => {
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getERC20TokenData").mockResolvedValueOnce({
+ ...tokenData,
+ symbol: "\0\0\0\0\0\0",
+ });
+ });
+
+ it("returns null", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress, transactionReceipt);
+ expect(token).toBeNull();
+ });
+ });
+
+ describe("when transactionReceipt param is not provided", () => {
+ it("returns the token without l1Address when token is valid", async () => {
+ const token = await tokenService.getERC20Token(deployedContractAddress);
+ expect(token).toStrictEqual({
+ ...tokenData,
+ blockNumber: deployedContractAddress.blockNumber,
+ transactionHash: deployedContractAddress.transactionHash,
+ l2Address: deployedContractAddress.address,
+ logIndex: deployedContractAddress.logIndex,
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/token/token.service.ts b/packages/data-fetcher/src/token/token.service.ts
new file mode 100644
index 0000000000..d1a9a35261
--- /dev/null
+++ b/packages/data-fetcher/src/token/token.service.ts
@@ -0,0 +1,121 @@
+import { types, utils } from "zksync-web3";
+import { Injectable, Logger } from "@nestjs/common";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { Histogram } from "prom-client";
+import { LogType, isLogOfType } from "../log/logType";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { GET_TOKEN_INFO_DURATION_METRIC_NAME } from "../metrics";
+import { ContractAddress } from "../address/interface/contractAddress.interface";
+import parseLog from "../utils/parseLog";
+import { CONTRACT_INTERFACES } from "../constants";
+
+export interface Token {
+ l2Address: string;
+ l1Address: string;
+ symbol: string;
+ decimals: number;
+ name: string;
+ blockNumber: number;
+ transactionHash: string;
+ logIndex: number;
+}
+
+export enum TokenType {
+ ETH = "ETH",
+ ERC20 = "ERC20",
+ ERC721 = "ERC721",
+}
+
+@Injectable()
+export class TokenService {
+ private readonly logger: Logger;
+
+ constructor(
+ private readonly blockchainService: BlockchainService,
+ @InjectMetric(GET_TOKEN_INFO_DURATION_METRIC_NAME)
+ private readonly getTokenInfoDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(TokenService.name);
+ }
+
+ private async getERC20TokenData(contractAddress: string): Promise<{
+ symbol: string;
+ decimals: number;
+ name: string;
+ }> {
+ try {
+ return await this.blockchainService.getERC20TokenData(contractAddress);
+ } catch {
+ this.logger.log({
+ message: "Cannot parse ERC20 contract. Might be a token of a different type.",
+ contractAddress,
+ });
+ return null;
+ }
+ }
+
+ private removeSpecialChars(str: string | null): string {
+ if (!str) {
+ return str;
+ }
+ return str.replace(/\0/g, "");
+ }
+
+ public async getERC20Token(
+ contractAddress: ContractAddress,
+ transactionReceipt?: types.TransactionReceipt
+ ): Promise {
+ let erc20Token: {
+ symbol: string;
+ decimals: number;
+ name: string;
+ l1Address?: string;
+ };
+
+ const bridgeLog =
+ transactionReceipt &&
+ transactionReceipt.to.toLowerCase() === this.blockchainService.bridgeAddresses.l2Erc20DefaultBridge &&
+ transactionReceipt.logs?.find(
+ (log) =>
+ isLogOfType(log, [LogType.BridgeInitialization, LogType.BridgeInitialize]) &&
+ log.address.toLowerCase() === contractAddress.address.toLowerCase()
+ );
+
+ if (bridgeLog) {
+ const parsedLog = parseLog(CONTRACT_INTERFACES.L2_STANDARD_ERC20, bridgeLog);
+ erc20Token = {
+ name: parsedLog.args.name,
+ symbol: parsedLog.args.symbol,
+ decimals: parsedLog.args.decimals,
+ l1Address: parsedLog.args.l1Token,
+ };
+ } else {
+ const stopGetTokenInfoDurationMetric = this.getTokenInfoDurationMetric.startTimer();
+ erc20Token = await this.getERC20TokenData(contractAddress.address);
+ if (erc20Token) {
+ stopGetTokenInfoDurationMetric();
+ }
+ }
+
+ if (erc20Token) {
+ erc20Token.symbol = this.removeSpecialChars(erc20Token.symbol);
+ erc20Token.name = this.removeSpecialChars(erc20Token.name);
+
+ if (erc20Token.symbol) {
+ return {
+ ...erc20Token,
+ blockNumber: contractAddress.blockNumber,
+ transactionHash: contractAddress.transactionHash,
+ l2Address: contractAddress.address,
+ logIndex: contractAddress.logIndex,
+ // add L1 address for ETH token
+ ...(contractAddress.address.toLowerCase() === utils.L2_ETH_TOKEN_ADDRESS && {
+ l1Address: utils.ETH_ADDRESS,
+ }),
+ };
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/packages/data-fetcher/src/transaction/index.ts b/packages/data-fetcher/src/transaction/index.ts
new file mode 100644
index 0000000000..6ac0ba14b0
--- /dev/null
+++ b/packages/data-fetcher/src/transaction/index.ts
@@ -0,0 +1 @@
+export * from "./transaction.service";
diff --git a/packages/data-fetcher/src/transaction/transaction.service.spec.ts b/packages/data-fetcher/src/transaction/transaction.service.spec.ts
new file mode 100644
index 0000000000..c8582d46e5
--- /dev/null
+++ b/packages/data-fetcher/src/transaction/transaction.service.spec.ts
@@ -0,0 +1,202 @@
+import { Test } from "@nestjs/testing";
+import { Logger } from "@nestjs/common";
+import { mock } from "jest-mock-extended";
+import { types } from "zksync-web3";
+import { BlockchainService, TraceTransactionResult } from "../blockchain";
+import { TransactionService } from "./transaction.service";
+import { LogService } from "../log";
+
+describe("TransactionService", () => {
+ let transactionProcessor: TransactionService;
+ let blockchainServiceMock: BlockchainService;
+ let logServiceMock: LogService;
+
+ let startTxProcessingDurationMetricMock: jest.Mock;
+ let stopTxProcessingDurationMetricMock: jest.Mock;
+
+ let startGetTransactionInfoDurationMetricMock: jest.Mock;
+ let stopGetTransactionInfoDurationMetricMock: jest.Mock;
+
+ beforeEach(async () => {
+ blockchainServiceMock = mock();
+ logServiceMock = mock();
+
+ stopTxProcessingDurationMetricMock = jest.fn();
+ startTxProcessingDurationMetricMock = jest.fn().mockReturnValue(stopTxProcessingDurationMetricMock);
+
+ stopGetTransactionInfoDurationMetricMock = jest.fn();
+ startGetTransactionInfoDurationMetricMock = jest.fn().mockReturnValue(stopGetTransactionInfoDurationMetricMock);
+
+ const app = await Test.createTestingModule({
+ providers: [
+ TransactionService,
+ {
+ provide: BlockchainService,
+ useValue: blockchainServiceMock,
+ },
+ {
+ provide: LogService,
+ useValue: logServiceMock,
+ },
+ {
+ provide: "PROM_METRIC_TRANSACTION_PROCESSING_DURATION_SECONDS",
+ useValue: {
+ startTimer: startTxProcessingDurationMetricMock,
+ },
+ },
+ {
+ provide: "PROM_METRIC_GET_TRANSACTION_INFO_DURATION_SECONDS",
+ useValue: {
+ startTimer: startGetTransactionInfoDurationMetricMock,
+ },
+ },
+ ],
+ }).compile();
+
+ app.useLogger(mock());
+
+ transactionProcessor = app.get(TransactionService);
+ });
+
+ describe("getData", () => {
+ const blockDetails = mock({
+ number: 1,
+ l1BatchNumber: 3,
+ });
+ const transaction = mock({ hash: "0" });
+ const transactionReceipt = mock({
+ transactionIndex: 0,
+ logs: [mock(), mock()],
+ status: 1,
+ });
+ const transactionDetails = mock();
+ const traceTransactionResult = mock({
+ error: "Some error",
+ revertReason: "Some revert reason",
+ });
+
+ beforeEach(() => {
+ jest.spyOn(blockchainServiceMock, "getTransaction").mockResolvedValue(transaction);
+ jest.spyOn(blockchainServiceMock, "getTransactionReceipt").mockResolvedValue(transactionReceipt);
+ jest.spyOn(blockchainServiceMock, "getTransactionDetails").mockResolvedValue(transactionDetails);
+ jest.spyOn(blockchainServiceMock, "debugTraceTransaction").mockResolvedValue(traceTransactionResult);
+ });
+
+ it("starts the transaction duration metric", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(startTxProcessingDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("starts the get info transaction duration metric", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(stopGetTransactionInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("reads transaction data by hash", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.getTransaction).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getTransaction).toHaveBeenCalledWith(transaction.hash);
+ });
+
+ it("reads transaction details by hash", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.getTransactionDetails).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getTransactionDetails).toHaveBeenCalledWith(transaction.hash);
+ });
+
+ it("reads transaction receipt by hash", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.getTransactionReceipt).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.getTransactionReceipt).toHaveBeenCalledWith(transaction.hash);
+ });
+
+ it("stops the get info transaction duration metric", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(stopGetTransactionInfoDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("throws error if transaction data by hash API returns null", async () => {
+ jest.spyOn(blockchainServiceMock, "getTransaction").mockResolvedValue(null);
+ await expect(transactionProcessor.getData(transaction.hash, blockDetails)).rejects.toThrowError(
+ new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transaction.hash}`)
+ );
+ });
+
+ it("throws error if transaction details by hash API returns null", async () => {
+ jest.spyOn(blockchainServiceMock, "getTransactionDetails").mockResolvedValue(null);
+ await expect(transactionProcessor.getData(transaction.hash, blockDetails)).rejects.toThrowError(
+ new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transaction.hash}`)
+ );
+ });
+
+ it("throws error if transaction receipt by hash API returns null", async () => {
+ jest.spyOn(blockchainServiceMock, "getTransactionReceipt").mockResolvedValue(null);
+ await expect(transactionProcessor.getData(transaction.hash, blockDetails)).rejects.toThrowError(
+ new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transaction.hash}`)
+ );
+ });
+
+ it("returns data with transaction info", async () => {
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transaction).toEqual({
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: transactionReceipt.status,
+ });
+ });
+
+ it("returns data with transaction receipt", async () => {
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transactionReceipt).toEqual(transactionReceipt);
+ });
+
+ it("stops the transaction duration metric", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(stopTxProcessingDurationMetricMock).toHaveBeenCalledTimes(1);
+ });
+
+ describe("when transaction has failed status", () => {
+ beforeEach(() => {
+ (blockchainServiceMock.getTransactionReceipt as jest.Mock).mockResolvedValueOnce({
+ transactionIndex: 0,
+ logs: [],
+ status: 0,
+ });
+ });
+
+ it("reads transaction trace", async () => {
+ await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(blockchainServiceMock.debugTraceTransaction).toHaveBeenCalledTimes(1);
+ expect(blockchainServiceMock.debugTraceTransaction).toHaveBeenCalledWith(transaction.hash, true);
+ });
+
+ describe("when transaction trace contains error and revert reason", () => {
+ it("returns data with transaction info with error and revert reason", async () => {
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transaction).toEqual({
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: 0,
+ error: traceTransactionResult.error,
+ revertReason: traceTransactionResult.revertReason,
+ });
+ });
+ });
+
+ describe("when transaction trace doe not contain error and revert reason", () => {
+ it("returns data with transaction info without error and revert reason", async () => {
+ (blockchainServiceMock.debugTraceTransaction as jest.Mock).mockResolvedValueOnce(null);
+ const txData = await transactionProcessor.getData(transaction.hash, blockDetails);
+ expect(txData.transaction).toEqual({
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: 0,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/transaction/transaction.service.ts b/packages/data-fetcher/src/transaction/transaction.service.ts
new file mode 100644
index 0000000000..3b0e31dc4d
--- /dev/null
+++ b/packages/data-fetcher/src/transaction/transaction.service.ts
@@ -0,0 +1,90 @@
+import { Injectable, Logger } from "@nestjs/common";
+import { InjectMetric } from "@willsoto/nestjs-prometheus";
+import { Histogram } from "prom-client";
+import { types } from "zksync-web3";
+import { BlockchainService } from "../blockchain/blockchain.service";
+import { TRANSACTION_PROCESSING_DURATION_METRIC_NAME, GET_TRANSACTION_INFO_DURATION_METRIC_NAME } from "../metrics";
+import { LogService, LogsData } from "../log/log.service";
+
+export interface TransactionInfo extends types.TransactionResponse {
+ fee: string;
+ receiptStatus: number;
+ isL1Originated: boolean;
+ receivedAt: Date;
+ error?: string;
+ revertReason?: string;
+}
+
+export interface TransactionData extends LogsData {
+ transaction: TransactionInfo;
+ transactionReceipt: types.TransactionReceipt;
+}
+
+@Injectable()
+export class TransactionService {
+ private readonly logger: Logger;
+
+ public constructor(
+ private readonly blockchainService: BlockchainService,
+ private readonly logService: LogService,
+ @InjectMetric(TRANSACTION_PROCESSING_DURATION_METRIC_NAME)
+ private readonly transactionProcessingDurationMetric: Histogram,
+ @InjectMetric(GET_TRANSACTION_INFO_DURATION_METRIC_NAME)
+ private readonly getTransactionInfoDurationMetric: Histogram
+ ) {
+ this.logger = new Logger(TransactionService.name);
+ }
+
+ public async getData(transactionHash: string, blockDetails: types.BlockDetails): Promise {
+ const stopTransactionProcessingMeasuring = this.transactionProcessingDurationMetric.startTimer();
+
+ this.logger.debug({
+ message: "Getting transaction data from the blockchain",
+ blockNumber: blockDetails.number,
+ transactionHash,
+ });
+ const stopGetTransactionInfoDurationMetric = this.getTransactionInfoDurationMetric.startTimer();
+ const [transaction, transactionDetails, transactionReceipt] = await Promise.all([
+ this.blockchainService.getTransaction(transactionHash),
+ this.blockchainService.getTransactionDetails(transactionHash),
+ this.blockchainService.getTransactionReceipt(transactionHash),
+ ]);
+ stopGetTransactionInfoDurationMetric();
+
+ if (!transaction || !transactionDetails || !transactionReceipt) {
+ throw new Error(`Some of the blockchain transaction APIs returned null for a transaction ${transactionHash}`);
+ }
+
+ const transactionInfo = {
+ ...transaction,
+ ...transactionDetails,
+ l1BatchNumber: blockDetails.l1BatchNumber,
+ receiptStatus: transactionReceipt.status,
+ } as TransactionInfo;
+
+ if (transactionReceipt.status === 0) {
+ const debugTraceTransactionResult = await this.blockchainService.debugTraceTransaction(transactionHash, true);
+ if (debugTraceTransactionResult?.error) {
+ transactionInfo.error = debugTraceTransactionResult.error;
+ }
+ if (debugTraceTransactionResult?.revertReason) {
+ transactionInfo.revertReason = debugTraceTransactionResult.revertReason;
+ }
+ }
+
+ const logsData = await this.logService.getData(
+ transactionReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ transactionReceipt
+ );
+
+ stopTransactionProcessingMeasuring();
+
+ return {
+ ...logsData,
+ transaction: transactionInfo,
+ transactionReceipt,
+ };
+ }
+}
diff --git a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
similarity index 97%
rename from packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
index 1c8f6eaa23..ecf5e96b74 100644
--- a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.spec.ts
@@ -2,8 +2,8 @@ import { BigNumber } from "ethers";
import { types, utils } from "zksync-web3";
import { mock } from "jest-mock-extended";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { defaultFinalizeDepositHandler } from "./default.handler";
describe("defaultFinalizeDepositHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
similarity index 91%
rename from packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
index db5f0f2c04..f1b5158449 100644
--- a/packages/worker/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/finalizeDeposit/default.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/index.ts b/packages/data-fetcher/src/transfer/extractHandlers/index.ts
similarity index 100%
rename from packages/worker/src/transfer/extractHandlers/index.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/index.ts
diff --git a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
similarity index 97%
rename from packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
index ff6441d3cd..88cfd8fbd9 100644
--- a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.spec.ts
@@ -1,8 +1,8 @@
import { BigNumber } from "ethers";
import { types, utils } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { ethMintFromL1Handler } from "./ethMintFromL1.handler";
describe("ethMintFromL1Handler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
similarity index 91%
rename from packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
index 7b0a0220d9..b113937558 100644
--- a/packages/worker/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/mint/ethMintFromL1.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
similarity index 98%
rename from packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
index 57e64e8949..7ed0b4aea7 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.spec.ts
@@ -1,8 +1,8 @@
import { BigNumber } from "ethers";
import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { contractDeployerTransferHandler } from "./contractDeployerTransfer.handler";
describe("contractDeployerTransferHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
similarity index 92%
rename from packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
index 76e0cbf12e..92c73dfb0d 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/contractDeployerTransfer.handler.ts
@@ -2,8 +2,8 @@ import { utils, types } from "zksync-web3";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
import { Transfer } from "../../interfaces/transfer.interface";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.spec.ts
similarity index 98%
rename from packages/worker/src/transfer/extractHandlers/transfer/default.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.spec.ts
index 579e25ef74..28b15d39ed 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.spec.ts
@@ -1,8 +1,8 @@
import { BigNumber } from "ethers";
import { types, utils } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { defaultTransferHandler } from "./default.handler";
describe("defaultTransferHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.ts
similarity index 93%
rename from packages/worker/src/transfer/extractHandlers/transfer/default.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.ts
index 1762bc2df8..e89f6f2602 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/default.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/default.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
similarity index 97%
rename from packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
index 38b74f0a14..7c9e1b4c25 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handle.spec.ts
@@ -2,8 +2,8 @@ import { BigNumber } from "ethers";
import { types } from "zksync-web3";
import { mock } from "jest-mock-extended";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { erc721TransferHandler } from "./erc721Transfer.handler";
describe("erc721TransferHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
similarity index 92%
rename from packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
index e7245281c2..d8b824e2dd 100644
--- a/packages/worker/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/transfer/erc721Transfer.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
similarity index 97%
rename from packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
index a279e45014..9d09253a1c 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.spec.ts
@@ -1,8 +1,8 @@
import { BigNumber } from "ethers";
import { types, utils } from "zksync-web3";
import { mock } from "jest-mock-extended";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { ethWithdrawalToL1Handler } from "./ethWithdrawalToL1.handler";
describe("ethWithdrawalToL1Handler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
similarity index 91%
rename from packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
index c561eb88db..c387c4b96a 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawal/ethWithdrawalToL1.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
similarity index 97%
rename from packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
index 76e6cec0e1..0ffdcecc46 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.spec.ts
@@ -2,8 +2,8 @@ import { BigNumber } from "ethers";
import { types, utils } from "zksync-web3";
import { mock } from "jest-mock-extended";
import { ZERO_HASH_64 } from "../../../constants";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { defaultWithdrawalInitiatedHandler } from "./default.handler";
describe("defaultWithdrawalInitiatedHandler", () => {
diff --git a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
similarity index 91%
rename from packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
rename to packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
index 287ccf10d4..9699ae3c8f 100644
--- a/packages/worker/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
+++ b/packages/data-fetcher/src/transfer/extractHandlers/withdrawalInitiated/default.handler.ts
@@ -1,8 +1,8 @@
import { utils, types } from "zksync-web3";
import { Transfer } from "../../interfaces/transfer.interface";
import { ExtractTransferHandler } from "../../interfaces/extractTransferHandler.interface";
-import { TransferType } from "../../../entities/transfer.entity";
-import { TokenType } from "../../../entities/token.entity";
+import { TransferType } from "../../transfer.service";
+import { TokenType } from "../../../token/token.service";
import { unixTimeToDate } from "../../../utils/date";
import parseLog from "../../../utils/parseLog";
import { CONTRACT_INTERFACES } from "../../../constants";
diff --git a/packages/worker/src/transfer/interfaces/extractTransferHandler.interface.ts b/packages/data-fetcher/src/transfer/interfaces/extractTransferHandler.interface.ts
similarity index 77%
rename from packages/worker/src/transfer/interfaces/extractTransferHandler.interface.ts
rename to packages/data-fetcher/src/transfer/interfaces/extractTransferHandler.interface.ts
index c61793b85c..9450bab230 100644
--- a/packages/worker/src/transfer/interfaces/extractTransferHandler.interface.ts
+++ b/packages/data-fetcher/src/transfer/interfaces/extractTransferHandler.interface.ts
@@ -1,9 +1,8 @@
import { types } from "zksync-web3";
-import { BridgeAddresses } from "../../blockchain";
import { Transfer } from "./transfer.interface";
export interface ExtractTransferHandler {
- matches: (log: types.Log, txReceipt?: types.TransactionReceipt, bridgeAddresses?: BridgeAddresses) => boolean;
+ matches: (log: types.Log, txReceipt?: types.TransactionReceipt) => boolean;
extract: (
log: types.Log,
blockDetails: types.BlockDetails,
diff --git a/packages/worker/src/transfer/interfaces/transfer.interface.ts b/packages/data-fetcher/src/transfer/interfaces/transfer.interface.ts
similarity index 78%
rename from packages/worker/src/transfer/interfaces/transfer.interface.ts
rename to packages/data-fetcher/src/transfer/interfaces/transfer.interface.ts
index b8197f9f15..944166210f 100644
--- a/packages/worker/src/transfer/interfaces/transfer.interface.ts
+++ b/packages/data-fetcher/src/transfer/interfaces/transfer.interface.ts
@@ -1,6 +1,6 @@
import { BigNumber } from "ethers";
-import { TransferType } from "../../entities/transfer.entity";
-import { TokenType } from "../../entities/token.entity";
+import { TransferType } from "../transfer.service";
+import { TokenType } from "../../token/token.service";
export interface TransferFields {
tokenId?: BigNumber;
diff --git a/packages/worker/src/transfer/transfer.service.spec.ts b/packages/data-fetcher/src/transfer/transfer.service.spec.ts
similarity index 85%
rename from packages/worker/src/transfer/transfer.service.spec.ts
rename to packages/data-fetcher/src/transfer/transfer.service.spec.ts
index fe5bc10411..f1beeab6e2 100644
--- a/packages/worker/src/transfer/transfer.service.spec.ts
+++ b/packages/data-fetcher/src/transfer/transfer.service.spec.ts
@@ -3,10 +3,9 @@ import { Logger } from "@nestjs/common";
import { mock } from "jest-mock-extended";
import { BigNumber } from "ethers";
import { types } from "zksync-web3";
-import { TransferRepository } from "../repositories";
import { TransferService } from "./transfer.service";
import { BlockchainService } from "../blockchain/blockchain.service";
-import { TokenType } from "../entities/token.entity";
+import { TokenType } from "../token/token.service";
import * as ethDepositNoFee from "../../test/transactionReceipts/eth/deposit-no-fee.json";
import * as ethDepositZeroValue from "../../test/transactionReceipts/eth/deposit-zero-value.json";
@@ -65,12 +64,9 @@ jest.mock("../logger", () => ({
const toTxReceipt = (receipt: any): types.TransactionReceipt => receipt as types.TransactionReceipt;
describe("TransferService", () => {
- let transferRepositoryMock: TransferRepository;
let transferService: TransferService;
beforeEach(async () => {
- transferRepositoryMock = mock();
-
const app = await Test.createTestingModule({
providers: [
TransferService,
@@ -83,10 +79,6 @@ describe("TransferService", () => {
},
},
},
- {
- provide: TransferRepository,
- useValue: transferRepositoryMock,
- },
],
}).compile();
@@ -95,7 +87,7 @@ describe("TransferService", () => {
transferService = app.get(TransferService);
});
- describe("saveTransfers", () => {
+ describe("getTransfers", () => {
const receivedAt = new Date();
const blockDetails = mock();
blockDetails.timestamp = new Date().getTime() / 1000;
@@ -104,16 +96,16 @@ describe("TransferService", () => {
});
transactionDetails.receivedAt = receivedAt;
- it("does not save transfers if no logs specified", async () => {
- await transferService.saveTransfers(null, null);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(0);
+ it("returns an empty array if no logs are specified", async () => {
+ const transfers = await transferService.getTransfers(null, null);
+ expect(transfers).toStrictEqual([]);
});
describe("eth", () => {
describe("deposit with no fee", () => {
const txReceipt = toTxReceipt(ethDepositNoFee);
- it("properly saves and returns transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x6f05b59d3b20000"),
@@ -162,22 +154,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("deposit", () => {
const txReceipt = toTxReceipt(ethDeposit);
- it("saves deposit, transfer, fee and refund transfers", async () => {
+ it("returns deposit, transfer, fee and refund transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -241,22 +231,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("deposit to different address", () => {
const txReceipt = toTxReceipt(ethDepositToDifferentAddress);
- it("saves deposit, transfer, fee and refund transfers", async () => {
+ it("returns deposit, transfer, fee and refund transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -320,22 +308,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("zero value deposit", () => {
const txReceipt = toTxReceipt(ethDepositZeroValue);
- it("saves fee and refund transfers", async () => {
+ it("returns fee and refund transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -369,22 +355,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("transfer", () => {
const txReceipt = toTxReceipt(ethTransfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x018034d06a6900"),
@@ -421,17 +405,15 @@ describe("TransferService", () => {
const txDetails = mock({ isL1Originated: false });
txDetails.receivedAt = receivedAt;
- const result = await transferService.saveTransfers(txReceipt.logs, blockDetails, txDetails, txReceipt);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ const transfers = await transferService.getTransfers(txReceipt.logs, blockDetails, txDetails, txReceipt);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("transfer to zero address", () => {
const txReceipt = toTxReceipt(ethTransferToZeroAddress);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x017e3f22499e00"),
@@ -465,22 +447,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("withdrawal", () => {
const txReceipt = toTxReceipt(ethWithdrawal);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x119f17fe16000"),
@@ -544,22 +524,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("withdrawal to different address", () => {
const txReceipt = toTxReceipt(ethWithdrawalToDifferentAddress);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x119f17fe16000"),
@@ -622,22 +600,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("zero value withdrawal", () => {
const txReceipt = toTxReceipt(ethWithdrawalZeroValue);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -685,22 +661,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("zero value withdrawal", () => {
const txReceipt = toTxReceipt(ethWithdrawalToDifferentAddress);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x119f17fe16000"),
@@ -763,15 +737,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -780,7 +752,7 @@ describe("TransferService", () => {
describe("deposit", () => {
const txReceipt = toTxReceipt(preApprovedErc20Deposit);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x989680"),
@@ -813,22 +785,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("transfer", () => {
const txReceipt = toTxReceipt(preApprovedErc20Transfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x018083f408d000"),
@@ -861,22 +831,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("withdrawal", () => {
const txReceipt = toTxReceipt(preApprovedErc20Withdrawal);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0277b0afc08300"),
@@ -924,22 +892,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("withdrawal to different address", () => {
const txReceipt = toTxReceipt(preApprovedErc20WithdrawalToDifferentAddress);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x027673d9c05b00"),
@@ -987,15 +953,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -1004,7 +968,7 @@ describe("TransferService", () => {
describe("deploy to L2", () => {
const txReceipt = toTxReceipt(greeterDeployToL2);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x01baa818335500"),
@@ -1022,22 +986,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("execute SetGreeting function", () => {
const txReceipt = toTxReceipt(greeterSetGreeting);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0134b578f8b200"),
@@ -1055,15 +1017,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -1072,7 +1032,7 @@ describe("TransferService", () => {
describe("deploy to L2", () => {
const txReceipt = toTxReceipt(erc20DeployToL2);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x420c98159bd600"),
@@ -1105,22 +1065,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("bridge deposit from L1", () => {
const txReceipt = toTxReceipt(erc20BridgeDepositFromL1);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x016345785d8a0000"),
@@ -1153,22 +1111,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("bridge deposit from L1 to different address", () => {
const txReceipt = toTxReceipt(erc20BridgeDepositFromL1ToDifferentAddress);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -1231,22 +1187,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("transfer", () => {
const txReceipt = toTxReceipt(erc20Transfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0152d41ffe1400"),
@@ -1279,22 +1233,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("withdrawal", () => {
const txReceipt = toTxReceipt(erc20Withdrawal);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -1357,15 +1309,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -1374,7 +1324,7 @@ describe("TransferService", () => {
describe("eth, USDC, ERC20 token multi transfer", () => {
const txReceipt = toTxReceipt(multiTransfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x04a061c5f0b100"),
@@ -1497,22 +1447,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("eth, USDC, ERC20 token multi transfer through Paymaster", () => {
const txReceipt = toTxReceipt(multiTransferThroughPaymaster);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0b2cf038c49e00"),
@@ -1650,22 +1598,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("multi eth transfer", () => {
const txReceipt = toTxReceipt(multiEthTransfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0b2cf038c49e00"),
@@ -1743,15 +1689,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -1760,7 +1704,7 @@ describe("TransferService", () => {
describe("deploy to l2", () => {
const txReceipt = toTxReceipt(nftDeploy);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x6e45eb3828c800"),
@@ -1778,22 +1722,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("mint", () => {
const txReceipt = toTxReceipt(nftMint);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x02570592a28a00"),
@@ -1827,22 +1769,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("approve", () => {
const txReceipt = toTxReceipt(nftApprove);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x011f05a0378000"),
@@ -1860,22 +1800,20 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("transfer", () => {
const txReceipt = toTxReceipt(nftTransfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x01bbaabf001a00"),
@@ -1909,15 +1847,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -1926,7 +1862,7 @@ describe("TransferService", () => {
describe("transfer", () => {
const txReceipt = toTxReceipt(paymasterTransfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0b2cf038c49e00"),
@@ -1959,15 +1895,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -1976,7 +1910,7 @@ describe("TransferService", () => {
describe("ERC20 transfer to zero address that fails", () => {
const txReceipt = toTxReceipt(failedErc20Transfer);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x1606ddfd9b8000"),
@@ -1994,24 +1928,27 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
describe("tx with no logs", () => {
- it("does not save transfers", async () => {
+ it("returns an empty array", async () => {
const txReceipt = toTxReceipt(txWithNoLogs);
- await transferService.saveTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(0);
+ const transfers = await transferService.getTransfers(
+ txReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ txReceipt
+ );
+ expect(transfers).toStrictEqual([]);
});
});
@@ -2019,7 +1956,7 @@ describe("TransferService", () => {
describe("subcalls to other contracts", () => {
const txReceipt = toTxReceipt(subCallsToOtherContracts);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x0220aed39b7400"),
@@ -2037,15 +1974,13 @@ describe("TransferService", () => {
timestamp: receivedAt,
},
];
- const result = await transferService.saveTransfers(
+ const transfers = await transferService.getTransfers(
txReceipt.logs,
blockDetails,
transactionDetails,
txReceipt
);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
@@ -2053,10 +1988,14 @@ describe("TransferService", () => {
describe("tx with supported event but with no matching handlers", () => {
const txReceipt = toTxReceipt(noMatchingHandlers);
- it("properly saves transfers", async () => {
- await transferService.saveTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith([
+ it("returns proper transfers", async () => {
+ const transfers = await transferService.getTransfers(
+ txReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ txReceipt
+ );
+ expect(transfers).toStrictEqual([
{
amount: BigNumber.from("0xcdab63f37000"),
blockNumber: 166322,
@@ -2107,11 +2046,15 @@ describe("TransferService", () => {
});
describe("when tx contains transfer logs with some args of type address which are out of range", () => {
- it("saves the transfer successfully", async () => {
+ it("returns proper transfers", async () => {
const txReceipt = toTxReceipt(addressOutOfRange);
- await transferService.saveTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith([
+ const transfers = await transferService.getTransfers(
+ txReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ txReceipt
+ );
+ expect(transfers).toStrictEqual([
{
blockNumber: 4985412,
fields: { tokenId: BigNumber.from("0x15") },
@@ -2126,6 +2069,7 @@ describe("TransferService", () => {
type: "mint",
isFeeOrRefund: false,
isInternal: false,
+ amount: undefined,
},
{
blockNumber: 4985412,
@@ -2141,6 +2085,7 @@ describe("TransferService", () => {
type: "mint",
isFeeOrRefund: false,
isInternal: false,
+ amount: undefined,
},
{
blockNumber: 4985412,
@@ -2156,6 +2101,7 @@ describe("TransferService", () => {
type: "transfer",
isFeeOrRefund: false,
isInternal: false,
+ amount: undefined,
},
]);
});
@@ -2164,17 +2110,16 @@ describe("TransferService", () => {
describe("when tx contains transfer log which fails to be parsed", () => {
it("throws an error", async () => {
const txReceipt = toTxReceipt(logParsingError);
- await expect(
- transferService.saveTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt)
- ).rejects.toThrowError();
- expect(transferRepositoryMock.addMany).not.toBeCalled();
+ expect(() =>
+ transferService.getTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt)
+ ).toThrowError();
});
});
describe("when there is no deposit after fee", () => {
const txReceipt = toTxReceipt(noDepositAfterFee);
- it("saves 0 refund refunds", async () => {
+ it("returns 0 refund refunds", async () => {
const expectedTransfers = [
{
from: "0xfb7e0856e44eff812a44a9f47733d7d55c39aa28",
@@ -2223,17 +2168,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ const transfers = await transferService.getTransfers(
+ txReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ txReceipt
+ );
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("when there are no deposits except fee", () => {
const txReceipt = toTxReceipt(feeWithNoDeposits);
- it("saves fee with initiator address", async () => {
+ it("returns fee with initiator address", async () => {
const expectedTransfers = [
{
from: "0xA38EDFcc55164a59e0f33918D13a2d559BC11df8",
@@ -2252,17 +2200,20 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(txReceipt.logs, blockDetails, transactionDetails, txReceipt);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ const transfers = await transferService.getTransfers(
+ txReceipt.logs,
+ blockDetails,
+ transactionDetails,
+ txReceipt
+ );
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("when no transaction data is passed", () => {
const txReceipt = toTxReceipt(greeterDeployToL2);
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const expectedTransfers = [
{
amount: BigNumber.from("0x01baa818335500"),
@@ -2280,15 +2231,13 @@ describe("TransferService", () => {
timestamp: new Date(blockDetails.timestamp * 1000),
},
];
- const result = await transferService.saveTransfers(txReceipt.logs, blockDetails);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ const transfers = await transferService.getTransfers(txReceipt.logs, blockDetails);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
describe("block with no transactions", () => {
- it("properly saves transfers", async () => {
+ it("returns proper transfers", async () => {
const blockDate = new Date();
blockDetails.timestamp = blockDate.getTime() / 1000;
@@ -2310,10 +2259,8 @@ describe("TransferService", () => {
},
];
- const result = await transferService.saveTransfers(blockWithNoTxsLogs, blockDetails);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledTimes(1);
- expect(transferRepositoryMock.addMany).toHaveBeenCalledWith(expectedTransfers);
- expect(result).toStrictEqual(expectedTransfers);
+ const transfers = await transferService.getTransfers(blockWithNoTxsLogs, blockDetails);
+ expect(transfers).toStrictEqual(expectedTransfers);
});
});
});
diff --git a/packages/worker/src/transfer/transfer.service.ts b/packages/data-fetcher/src/transfer/transfer.service.ts
similarity index 88%
rename from packages/worker/src/transfer/transfer.service.ts
rename to packages/data-fetcher/src/transfer/transfer.service.ts
index 09a553272a..b4191a4915 100644
--- a/packages/worker/src/transfer/transfer.service.ts
+++ b/packages/data-fetcher/src/transfer/transfer.service.ts
@@ -1,9 +1,6 @@
import { types, utils } from "zksync-web3";
import { Injectable, Logger } from "@nestjs/common";
import { LogType } from "../log/logType";
-import { BlockchainService } from "../blockchain/blockchain.service";
-import { TransferRepository } from "../repositories";
-import { TransferType } from "../entities";
import isInternalTransaction from "../utils/isInternalTransaction";
import { ExtractTransferHandler } from "./interfaces/extractTransferHandler.interface";
import { Transfer } from "./interfaces/transfer.interface";
@@ -17,6 +14,15 @@ import {
ethWithdrawalToL1Handler,
} from "./extractHandlers";
+export enum TransferType {
+ Deposit = "deposit",
+ Transfer = "transfer",
+ Withdrawal = "withdrawal",
+ Fee = "fee",
+ Mint = "mint",
+ Refund = "refund",
+}
+
const extractTransfersHandlers: Record = {
[LogType.FinalizeDeposit]: [defaultFinalizeDepositHandler],
[LogType.WithdrawalInitiated]: [defaultWithdrawalInitiatedHandler],
@@ -29,27 +35,24 @@ const extractTransfersHandlers: Record = {
export class TransferService {
private readonly logger: Logger;
- constructor(
- private readonly blockchainService: BlockchainService,
- private readonly transferRepository: TransferRepository
- ) {
+ constructor() {
this.logger = new Logger(TransferService.name);
}
- public async saveTransfers(
+ public getTransfers(
logs: types.Log[],
blockDetails: types.BlockDetails,
transactionDetails?: types.TransactionDetails,
transactionReceipt?: types.TransactionReceipt
- ): Promise {
+ ): Transfer[] {
+ const transfers: Transfer[] = [];
if (!logs) {
- return;
+ return transfers;
}
- const transfers: Transfer[] = [];
for (const log of logs) {
const handlerForLog = extractTransfersHandlers[log.topics[0]]?.find((handler) =>
- handler.matches(log, transactionReceipt, this.blockchainService.bridgeAddresses)
+ handler.matches(log, transactionReceipt)
);
if (!handlerForLog) {
@@ -75,7 +78,6 @@ export class TransferService {
if (transfers.length) {
this.formatFeeAndRefundDeposits(transfers, transactionDetails);
this.markInternalTransactions(transfers, transactionReceipt);
- await this.transferRepository.addMany(transfers);
}
return transfers;
diff --git a/packages/data-fetcher/src/utils/date.spec.ts b/packages/data-fetcher/src/utils/date.spec.ts
new file mode 100644
index 0000000000..55f5285f59
--- /dev/null
+++ b/packages/data-fetcher/src/utils/date.spec.ts
@@ -0,0 +1,8 @@
+import { unixTimeToDate } from "./date";
+
+describe("unixTimeToDate", () => {
+ it("formats unix time to Date", () => {
+ const date = new Date();
+ expect(unixTimeToDate(date.getTime() / 1000)).toEqual(date);
+ });
+});
diff --git a/packages/data-fetcher/src/utils/date.ts b/packages/data-fetcher/src/utils/date.ts
new file mode 100644
index 0000000000..6df1dfea38
--- /dev/null
+++ b/packages/data-fetcher/src/utils/date.ts
@@ -0,0 +1 @@
+export const unixTimeToDate = (unixTime: number) => new Date(unixTime * 1000);
diff --git a/packages/worker/src/utils/isInternalTransaction.spec.ts b/packages/data-fetcher/src/utils/isInternalTransaction.spec.ts
similarity index 97%
rename from packages/worker/src/utils/isInternalTransaction.spec.ts
rename to packages/data-fetcher/src/utils/isInternalTransaction.spec.ts
index 878d4f0c0f..3990be8302 100644
--- a/packages/worker/src/utils/isInternalTransaction.spec.ts
+++ b/packages/data-fetcher/src/utils/isInternalTransaction.spec.ts
@@ -1,6 +1,6 @@
import { types, utils } from "zksync-web3";
import { Transfer } from "../transfer/interfaces/transfer.interface";
-import { TransferType } from "../entities/transfer.entity";
+import { TransferType } from "../transfer/transfer.service";
import isInternalTransaction from "./isInternalTransaction";
describe("isInternalTransaction", () => {
diff --git a/packages/worker/src/utils/isInternalTransaction.ts b/packages/data-fetcher/src/utils/isInternalTransaction.ts
similarity index 90%
rename from packages/worker/src/utils/isInternalTransaction.ts
rename to packages/data-fetcher/src/utils/isInternalTransaction.ts
index 72236cf119..1283e74ee5 100644
--- a/packages/worker/src/utils/isInternalTransaction.ts
+++ b/packages/data-fetcher/src/utils/isInternalTransaction.ts
@@ -1,6 +1,6 @@
import { types, utils } from "zksync-web3";
import { Transfer } from "../transfer/interfaces/transfer.interface";
-import { TransferType } from "../entities/transfer.entity";
+import { TransferType } from "../transfer/transfer.service";
export default function isInternalTransaction(transfer: Transfer, transactionReceipt?: types.TransactionReceipt) {
if (transfer.type !== TransferType.Transfer) {
diff --git a/packages/data-fetcher/src/utils/parseLog.spec.ts b/packages/data-fetcher/src/utils/parseLog.spec.ts
new file mode 100644
index 0000000000..143422c048
--- /dev/null
+++ b/packages/data-fetcher/src/utils/parseLog.spec.ts
@@ -0,0 +1,397 @@
+import { mock } from "jest-mock-extended";
+import { utils } from "ethers";
+import { types } from "zksync-web3";
+import parseLog from "./parseLog";
+
+jest.mock("../logger", () => ({
+ default: {
+ error: jest.fn(),
+ },
+}));
+
+describe("parseLog", () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("parses log", () => {
+ const log = mock({
+ topics: [],
+ });
+ const parsedLog = mock({ args: {} });
+ const contractInterface = mock({
+ parseLog: jest.fn().mockReturnValue(parsedLog),
+ });
+
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ });
+
+ describe("when some of the arguments fail to be parsed", () => {
+ let contractInterface;
+ let parsedLog;
+ beforeEach(() => {
+ parsedLog = {
+ args: {
+ from: "from",
+ get to() {
+ throw new Error("failed to parse");
+ },
+ },
+ };
+ contractInterface = mock({
+ parseLog: jest
+ .fn()
+ .mockReturnValueOnce(parsedLog)
+ .mockReturnValueOnce({
+ args: {
+ from: "from",
+ amount: "amount",
+ to: "to",
+ },
+ }),
+ });
+ });
+
+ describe("and parsed log does not have eventFragment", () => {
+ it("returns parsed log as it is", () => {
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and parsed log has eventFragment with type different to event", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "function",
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and parsed log has eventFragment with no inputs", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and parsed log has eventFragment with empty inputs array", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [],
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and parsed log has eventFragment with event type and inputs", () => {
+ describe("and parser throws an error with no error details", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "to",
+ },
+ ],
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and parser throws an error with error reason different than value out of range", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "to",
+ },
+ ],
+ };
+ parsedLog.args = {
+ get to() {
+ throw {
+ error: {
+ reason: "unknown",
+ type: "address",
+ },
+ };
+ },
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and parser throws an error with error type not equal to address", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "to",
+ },
+ ],
+ };
+ parsedLog.args = {
+ get to() {
+ throw {
+ error: {
+ reason: "value out of range",
+ type: "hex",
+ },
+ };
+ },
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and there is no input with name matching the parser error name", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "to",
+ },
+ ],
+ };
+ parsedLog.args = {
+ get to() {
+ throw {
+ error: {
+ reason: "value out of range",
+ type: "address",
+ name: "from",
+ },
+ };
+ },
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and failed arg is not indexed", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "to",
+ indexed: false,
+ },
+ ],
+ };
+ parsedLog.args = {
+ get to() {
+ throw {
+ error: {
+ reason: "value out of range",
+ type: "address",
+ name: "to",
+ },
+ };
+ },
+ };
+ const log = mock({ topics: [] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and there is no topic found for failed arg", () => {
+ it("returns parsed log as it is", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "from",
+ indexed: true,
+ },
+ {
+ name: "to",
+ indexed: true,
+ },
+ ],
+ };
+ parsedLog.args = {
+ from: "from",
+ get to() {
+ throw {
+ error: {
+ reason: "value out of range",
+ type: "address",
+ name: "to",
+ },
+ };
+ },
+ };
+ const log = mock({ topics: ["topic0", "topic1"] });
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log
+ );
+ expect(result).toBe(parsedLog);
+ expect(contractInterface.parseLog).toBeCalledTimes(1);
+ });
+ });
+
+ describe("and there is a topic for failed arg", () => {
+ it("fixes out of range address args and returns parsed log", () => {
+ parsedLog.eventFragment = {
+ type: "event",
+ inputs: [
+ {
+ name: "from",
+ indexed: true,
+ },
+ {
+ name: "amount",
+ indexed: true,
+ },
+ {
+ name: "to",
+ indexed: true,
+ },
+ ],
+ };
+ parsedLog.args = {
+ amount: "amount",
+ get from() {
+ throw {
+ error: {
+ reason: "value out of range",
+ type: "address",
+ name: "from",
+ },
+ };
+ },
+ get to() {
+ throw {
+ error: {
+ reason: "value out of range",
+ type: "address",
+ name: "to",
+ },
+ };
+ },
+ };
+ const log = {
+ logIndex: 1,
+ topics: [
+ "topic0",
+ "0x00000000000000000000001438686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ "topic2",
+ "0x00000000000000000000001548686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ ],
+ };
+ const result = parseLog(
+ {
+ interface: contractInterface,
+ },
+ log as types.Log
+ );
+ expect(result).toEqual({
+ args: {
+ from: "from",
+ amount: "amount",
+ to: "to",
+ },
+ });
+ expect(contractInterface.parseLog).toBeCalledTimes(2);
+ expect(contractInterface.parseLog).toBeCalledWith(log);
+ expect(contractInterface.parseLog).toBeCalledWith({
+ logIndex: 1,
+ topics: [
+ "topic0",
+ "0x00000000000000000000000038686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ "topic2",
+ "0x00000000000000000000000048686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ ],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/data-fetcher/src/utils/parseLog.ts b/packages/data-fetcher/src/utils/parseLog.ts
new file mode 100644
index 0000000000..3a999f1e0f
--- /dev/null
+++ b/packages/data-fetcher/src/utils/parseLog.ts
@@ -0,0 +1,63 @@
+import { utils } from "ethers";
+import { types } from "zksync-web3";
+import logger from "../logger";
+
+const ADDRESS_LENGTH = 40;
+
+export default function parseLog(
+ contractInterface: { interface: utils.Interface },
+ log: types.Log
+): utils.LogDescription {
+ const logDescription = contractInterface.interface.parseLog(log);
+
+ // All the logic below is to handle specific solc versions issue causing args of type address to be out of range sometimes
+ if (logDescription.eventFragment?.type !== "event" || !logDescription.eventFragment.inputs?.length) {
+ return logDescription;
+ }
+
+ let hasAddressOutOfRangeErrors = false;
+ const topics = [...log.topics];
+
+ Object.keys(logDescription.args).forEach((arg) => {
+ try {
+ // accessing property to throw deferred error if any
+ logDescription.args[arg];
+ } catch (error) {
+ logger.error("Failed to parse log argument", {
+ error,
+ blockNumber: log.blockNumber,
+ logIndex: log.logIndex,
+ transactionHash: log.transactionHash,
+ arg,
+ });
+ if (!error.error) {
+ return;
+ }
+ if (error.error.reason !== "value out of range" || error.error.type !== "address") {
+ return;
+ }
+ const inputIndex = logDescription.eventFragment.inputs.findIndex((input) => input.name === error.error.name);
+ if (inputIndex === -1) {
+ return;
+ }
+ const input = logDescription.eventFragment.inputs[inputIndex];
+ if (!input.indexed) {
+ return;
+ }
+ // incrementing inputIndex to get topicIndex as the first topic is event signature
+ const topicIndex = inputIndex + 1;
+ if (!topics[topicIndex]) {
+ return;
+ }
+ topics[topicIndex] = `0x000000000000000000000000${topics[topicIndex].slice(-ADDRESS_LENGTH)}`;
+ hasAddressOutOfRangeErrors = true;
+ }
+ });
+
+ return hasAddressOutOfRangeErrors
+ ? contractInterface.interface.parseLog({
+ ...log,
+ topics,
+ })
+ : logDescription;
+}
diff --git a/packages/data-fetcher/temp-response-new.json b/packages/data-fetcher/temp-response-new.json
new file mode 100644
index 0000000000..67863efcfe
--- /dev/null
+++ b/packages/data-fetcher/temp-response-new.json
@@ -0,0 +1,534 @@
+[
+ {
+ "block": {
+ "hash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "parentHash": "0xf51ddd9b9e7f41db08c4675e7266f416fa6e3bc255244e6aeb4da3e6f4e5ee9f",
+ "number": 14360,
+ "timestamp": 1703008573,
+ "nonce": "0x0000000000000000",
+ "difficulty": 0,
+ "gasLimit": "4294967295",
+ "gasUsed": "218166",
+ "miner": "0x0000000000000000000000000000000000000000",
+ "extraData": "0x",
+ "transactions": [
+ "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2"
+ ],
+ "baseFeePerGas": "190000000",
+ "l1BatchNumber": 4495,
+ "l1BatchTimestamp": 1703008573,
+ "_difficulty": "0"
+ },
+ "blockDetails": {
+ "number": 14360,
+ "l1BatchNumber": 4495,
+ "timestamp": 1703008573,
+ "l1TxCount": 0,
+ "l2TxCount": 1,
+ "rootHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "status": "verified",
+ "commitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "committedAt": "2023-12-19T19:01:02.399115Z",
+ "proveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "provenAt": "2023-12-20T09:24:50.626627Z",
+ "executeTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "executedAt": "2023-12-20T09:24:50.683974Z",
+ "l1GasPrice": 13227383562,
+ "l2FairGasPrice": 190000000,
+ "baseSystemContractsHashes": {
+ "bootloader": "0x01000831ba7021800f5d9103772fcc7463ed7e764a2a3624cacca6b3826172d0",
+ "default_aa": "0x0100055bf7f1bc4237c2be24252fb6737cc235194139e544933c1dbf25c24ee8"
+ },
+ "operatorAddress": "0x381b5cbd43f30a88ac2c7d8ed021325443156455",
+ "protocolVersion": "Version19"
+ },
+ "transactions": [
+ {
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000047cdbe811b80",
+ "logIndex": 0,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T17:56:13.518Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T17:56:13.518Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000221a8e2fee80",
+ "logIndex": 2,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T17:56:13.518Z"
+ }
+ ],
+ "transfers": [
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0x0000000000000000000000000000000000008001",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "blockNumber": 14360,
+ "amount": "78948990000000",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "fee",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 0,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T17:56:13.518Z",
+ "isInternal": false
+ },
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "blockNumber": 14360,
+ "amount": "1",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "transfer",
+ "tokenType": "ETH",
+ "isFeeOrRefund": false,
+ "logIndex": 1,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T17:56:13.518Z",
+ "isInternal": false
+ },
+ {
+ "from": "0x0000000000000000000000000000000000008001",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "blockNumber": 14360,
+ "amount": "37497450000000",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "refund",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 2,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T17:56:13.518Z",
+ "isInternal": false
+ }
+ ],
+ "contractAddresses": [],
+ "tokens": [],
+ "transaction": {
+ "hash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "type": 0,
+ "accessList": null,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "blockNumber": 14360,
+ "transactionIndex": 0,
+ "confirmations": 1248,
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "gasPrice": "190000000",
+ "maxPriorityFeePerGas": "190000000",
+ "maxFeePerGas": "190000000",
+ "gasLimit": "415521",
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "value": "1",
+ "nonce": 4388,
+ "data": "0x",
+ "r": "0xdfed847b0fe3c8bfef93c574829a007d3e83b2c89c01d49bc472ba83f599c54b",
+ "s": "0x292a56260cb3e24a7f4b7306f59bdba3be87a037bb3f1fc93b294068e1f95217",
+ "v": 636,
+ "creates": null,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 0,
+ "chainId": 300,
+ "isL1Originated": false,
+ "status": "verified",
+ "fee": "0x25b330512d00",
+ "gasPerPubdata": "0x4e20",
+ "initiatorAddress": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "receivedAt": "2023-12-19T17:56:13.518Z",
+ "ethCommitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "ethProveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "ethExecuteTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "receiptStatus": 1
+ },
+ "transactionReceipt": {
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "gasUsed": "218166",
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000047cdbe811b80",
+ "logIndex": 0,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000221a8e2fee80",
+ "logIndex": 2,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495
+ }
+ ],
+ "blockNumber": 14360,
+ "confirmations": 1248,
+ "cumulativeGasUsed": "0",
+ "effectiveGasPrice": "190000000",
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 0,
+ "l2ToL1Logs": [],
+ "byzantium": true
+ }
+ }
+ ],
+ "changedBalances": [
+ {
+ "address": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14360,
+ "balance": "735838188348532885",
+ "tokenType": "ETH"
+ },
+ {
+ "address": "0x0000000000000000000000000000000000008001",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14360,
+ "balance": "41451540000000",
+ "tokenType": "ETH"
+ }
+ ]
+ },
+ {
+ "block": {
+ "hash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "parentHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "number": 14361,
+ "timestamp": 1703008747,
+ "nonce": "0x0000000000000000",
+ "difficulty": 0,
+ "gasLimit": "4294967295",
+ "gasUsed": "176103",
+ "miner": "0x0000000000000000000000000000000000000000",
+ "extraData": "0x",
+ "transactions": [
+ "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e"
+ ],
+ "baseFeePerGas": "190000000",
+ "l1BatchNumber": 4495,
+ "l1BatchTimestamp": 1703008573,
+ "_difficulty": "0"
+ },
+ "blockDetails": {
+ "number": 14361,
+ "l1BatchNumber": 4495,
+ "timestamp": 1703008747,
+ "l1TxCount": 0,
+ "l2TxCount": 1,
+ "rootHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "status": "verified",
+ "commitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "committedAt": "2023-12-19T19:01:02.399115Z",
+ "proveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "provenAt": "2023-12-20T09:24:50.626627Z",
+ "executeTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "executedAt": "2023-12-20T09:24:50.683974Z",
+ "l1GasPrice": 13227383562,
+ "l2FairGasPrice": 190000000,
+ "baseSystemContractsHashes": {
+ "bootloader": "0x01000831ba7021800f5d9103772fcc7463ed7e764a2a3624cacca6b3826172d0",
+ "default_aa": "0x0100055bf7f1bc4237c2be24252fb6737cc235194139e544933c1dbf25c24ee8"
+ },
+ "operatorAddress": "0x381b5cbd43f30a88ac2c7d8ed021325443156455",
+ "protocolVersion": "Version19"
+ },
+ "transactions": [
+ {
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000004643d9c01f80",
+ "logIndex": 0,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T18:01:15.030Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T18:01:15.030Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000027d56ff75f00",
+ "logIndex": 2,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T18:01:15.030Z"
+ }
+ ],
+ "transfers": [
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0x0000000000000000000000000000000000008001",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "blockNumber": 14361,
+ "amount": "77257230000000",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "fee",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 0,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T18:01:15.030Z",
+ "isInternal": false
+ },
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "blockNumber": 14361,
+ "amount": "1",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "transfer",
+ "tokenType": "ETH",
+ "isFeeOrRefund": false,
+ "logIndex": 1,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T18:01:15.030Z",
+ "isInternal": false
+ },
+ {
+ "from": "0x0000000000000000000000000000000000008001",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "blockNumber": 14361,
+ "amount": "43797660000000",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "refund",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 2,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T18:01:15.030Z",
+ "isInternal": false
+ }
+ ],
+ "contractAddresses": [],
+ "tokens": [],
+ "transaction": {
+ "hash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "type": 0,
+ "accessList": null,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "blockNumber": 14361,
+ "transactionIndex": 0,
+ "confirmations": 1247,
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "gasPrice": "190000000",
+ "maxPriorityFeePerGas": "190000000",
+ "maxFeePerGas": "190000000",
+ "gasLimit": "406617",
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "value": "1",
+ "nonce": 4389,
+ "data": "0x",
+ "r": "0xd0d163e1e723456684597da2dc294a609a5306dc7ed09bb2c21c5b2b35d50959",
+ "s": "0x3a1e3f9bb44f5dd04233179835855e8353c8a66404d166fec0c18896477ff681",
+ "v": 636,
+ "creates": null,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 1,
+ "chainId": 300,
+ "isL1Originated": false,
+ "status": "verified",
+ "fee": "0x1e6e69c8c080",
+ "gasPerPubdata": "0x4e20",
+ "initiatorAddress": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "receivedAt": "2023-12-19T18:01:15.030Z",
+ "ethCommitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "ethProveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "ethExecuteTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "receiptStatus": 1
+ },
+ "transactionReceipt": {
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "gasUsed": "176103",
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000004643d9c01f80",
+ "logIndex": 0,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000027d56ff75f00",
+ "logIndex": 2,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495
+ }
+ ],
+ "blockNumber": 14361,
+ "confirmations": 1247,
+ "cumulativeGasUsed": "0",
+ "effectiveGasPrice": "190000000",
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 1,
+ "l2ToL1Logs": [],
+ "byzantium": true
+ }
+ }
+ ],
+ "changedBalances": [
+ {
+ "address": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14361,
+ "balance": "735804728778532885",
+ "tokenType": "ETH"
+ },
+ {
+ "address": "0x0000000000000000000000000000000000008001",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14361,
+ "balance": "74911110000000",
+ "tokenType": "ETH"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/data-fetcher/temp-response-old.json b/packages/data-fetcher/temp-response-old.json
new file mode 100644
index 0000000000..c4a02fc874
--- /dev/null
+++ b/packages/data-fetcher/temp-response-old.json
@@ -0,0 +1,636 @@
+[
+ {
+ "block": {
+ "hash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "parentHash": "0xf51ddd9b9e7f41db08c4675e7266f416fa6e3bc255244e6aeb4da3e6f4e5ee9f",
+ "number": 14360,
+ "timestamp": 1703008573,
+ "nonce": "0x0000000000000000",
+ "difficulty": 0,
+ "gasLimit": {
+ "type": "BigNumber",
+ "hex": "0xffffffff"
+ },
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x035436"
+ },
+ "miner": "0x0000000000000000000000000000000000000000",
+ "extraData": "0x",
+ "transactions": [
+ "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2"
+ ],
+ "baseFeePerGas": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "l1BatchNumber": 4495,
+ "l1BatchTimestamp": 1703008573,
+ "_difficulty": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ }
+ },
+ "blockDetails": {
+ "number": 14360,
+ "l1BatchNumber": 4495,
+ "timestamp": 1703008573,
+ "l1TxCount": 0,
+ "l2TxCount": 1,
+ "rootHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "status": "verified",
+ "commitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "committedAt": "2023-12-19T19:01:02.399115Z",
+ "proveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "provenAt": "2023-12-20T09:24:50.626627Z",
+ "executeTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "executedAt": "2023-12-20T09:24:50.683974Z",
+ "l1GasPrice": 13227383562,
+ "l2FairGasPrice": 190000000,
+ "baseSystemContractsHashes": {
+ "bootloader": "0x01000831ba7021800f5d9103772fcc7463ed7e764a2a3624cacca6b3826172d0",
+ "default_aa": "0x0100055bf7f1bc4237c2be24252fb6737cc235194139e544933c1dbf25c24ee8"
+ },
+ "operatorAddress": "0x381b5cbd43f30a88ac2c7d8ed021325443156455",
+ "protocolVersion": "Version19"
+ },
+ "transactions": [
+ {
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000047cdbe811b80",
+ "logIndex": 0,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T17:56:13.518Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T17:56:13.518Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000221a8e2fee80",
+ "logIndex": 2,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T17:56:13.518Z"
+ }
+ ],
+ "transfers": [
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0x0000000000000000000000000000000000008001",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "blockNumber": 14360,
+ "amount": {
+ "type": "BigNumber",
+ "hex": "0x47cdbe811b80"
+ },
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "fee",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 0,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T17:56:13.518Z",
+ "isInternal": false
+ },
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "blockNumber": 14360,
+ "amount": {
+ "type": "BigNumber",
+ "hex": "0x01"
+ },
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "transfer",
+ "tokenType": "ETH",
+ "isFeeOrRefund": false,
+ "logIndex": 1,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T17:56:13.518Z",
+ "isInternal": false
+ },
+ {
+ "from": "0x0000000000000000000000000000000000008001",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "blockNumber": 14360,
+ "amount": {
+ "type": "BigNumber",
+ "hex": "0x221a8e2fee80"
+ },
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "refund",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 2,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T17:56:13.518Z",
+ "isInternal": false
+ }
+ ],
+ "contractAddresses": [],
+ "tokens": [],
+ "transaction": {
+ "hash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "type": 0,
+ "accessList": null,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "blockNumber": 14360,
+ "transactionIndex": 0,
+ "confirmations": 1248,
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "gasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "maxPriorityFeePerGas": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "maxFeePerGas": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "gasLimit": {
+ "type": "BigNumber",
+ "hex": "0x065721"
+ },
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "value": {
+ "type": "BigNumber",
+ "hex": "0x01"
+ },
+ "nonce": 4388,
+ "data": "0x",
+ "r": "0xdfed847b0fe3c8bfef93c574829a007d3e83b2c89c01d49bc472ba83f599c54b",
+ "s": "0x292a56260cb3e24a7f4b7306f59bdba3be87a037bb3f1fc93b294068e1f95217",
+ "v": 636,
+ "creates": null,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 0,
+ "chainId": 300,
+ "isL1Originated": false,
+ "status": "verified",
+ "fee": "0x25b330512d00",
+ "gasPerPubdata": "0x4e20",
+ "initiatorAddress": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "receivedAt": "2023-12-19T17:56:13.518Z",
+ "ethCommitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "ethProveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "ethExecuteTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "receiptStatus": 1
+ },
+ "transactionReceipt": {
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x035436"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000047cdbe811b80",
+ "logIndex": 0,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14360,
+ "transactionHash": "0x32c35e0bf5076712ea7982ae573da291d4aefddcb5a57fc8c5be9e96c593e9c2",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000221a8e2fee80",
+ "logIndex": 2,
+ "blockHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "l1BatchNumber": 4495
+ }
+ ],
+ "blockNumber": 14360,
+ "confirmations": 1248,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 0,
+ "l2ToL1Logs": [],
+ "byzantium": true
+ }
+ }
+ ],
+ "changedBalances": [
+ {
+ "address": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14360,
+ "balance": {
+ "type": "BigNumber",
+ "hex": "0x0a3638ef04ae6895"
+ },
+ "tokenType": "ETH"
+ },
+ {
+ "address": "0x0000000000000000000000000000000000008001",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14360,
+ "balance": {
+ "type": "BigNumber",
+ "hex": "0x25b330512d00"
+ },
+ "tokenType": "ETH"
+ }
+ ]
+ },
+ {
+ "block": {
+ "hash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "parentHash": "0x4ff36eaafc4ad3f777849432f8aa72e0304fa8ee4b454cb5c672ffa8ae6c996e",
+ "number": 14361,
+ "timestamp": 1703008747,
+ "nonce": "0x0000000000000000",
+ "difficulty": 0,
+ "gasLimit": {
+ "type": "BigNumber",
+ "hex": "0xffffffff"
+ },
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x02afe7"
+ },
+ "miner": "0x0000000000000000000000000000000000000000",
+ "extraData": "0x",
+ "transactions": [
+ "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e"
+ ],
+ "baseFeePerGas": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "l1BatchNumber": 4495,
+ "l1BatchTimestamp": 1703008573,
+ "_difficulty": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ }
+ },
+ "blockDetails": {
+ "number": 14361,
+ "l1BatchNumber": 4495,
+ "timestamp": 1703008747,
+ "l1TxCount": 0,
+ "l2TxCount": 1,
+ "rootHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "status": "verified",
+ "commitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "committedAt": "2023-12-19T19:01:02.399115Z",
+ "proveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "provenAt": "2023-12-20T09:24:50.626627Z",
+ "executeTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "executedAt": "2023-12-20T09:24:50.683974Z",
+ "l1GasPrice": 13227383562,
+ "l2FairGasPrice": 190000000,
+ "baseSystemContractsHashes": {
+ "bootloader": "0x01000831ba7021800f5d9103772fcc7463ed7e764a2a3624cacca6b3826172d0",
+ "default_aa": "0x0100055bf7f1bc4237c2be24252fb6737cc235194139e544933c1dbf25c24ee8"
+ },
+ "operatorAddress": "0x381b5cbd43f30a88ac2c7d8ed021325443156455",
+ "protocolVersion": "Version19"
+ },
+ "transactions": [
+ {
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000004643d9c01f80",
+ "logIndex": 0,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T18:01:15.030Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T18:01:15.030Z"
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000027d56ff75f00",
+ "logIndex": 2,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495,
+ "timestamp": "2023-12-19T18:01:15.030Z"
+ }
+ ],
+ "transfers": [
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0x0000000000000000000000000000000000008001",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "blockNumber": 14361,
+ "amount": {
+ "type": "BigNumber",
+ "hex": "0x4643d9c01f80"
+ },
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "fee",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 0,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T18:01:15.030Z",
+ "isInternal": false
+ },
+ {
+ "from": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "blockNumber": 14361,
+ "amount": {
+ "type": "BigNumber",
+ "hex": "0x01"
+ },
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "transfer",
+ "tokenType": "ETH",
+ "isFeeOrRefund": false,
+ "logIndex": 1,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T18:01:15.030Z",
+ "isInternal": false
+ },
+ {
+ "from": "0x0000000000000000000000000000000000008001",
+ "to": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "blockNumber": 14361,
+ "amount": {
+ "type": "BigNumber",
+ "hex": "0x27d56ff75f00"
+ },
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "type": "refund",
+ "tokenType": "ETH",
+ "isFeeOrRefund": true,
+ "logIndex": 2,
+ "transactionIndex": 0,
+ "timestamp": "2023-12-19T18:01:15.030Z",
+ "isInternal": false
+ }
+ ],
+ "contractAddresses": [],
+ "tokens": [],
+ "transaction": {
+ "hash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "type": 0,
+ "accessList": null,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "blockNumber": 14361,
+ "transactionIndex": 0,
+ "confirmations": 1247,
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "gasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "maxPriorityFeePerGas": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "maxFeePerGas": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "gasLimit": {
+ "type": "BigNumber",
+ "hex": "0x063459"
+ },
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "value": {
+ "type": "BigNumber",
+ "hex": "0x01"
+ },
+ "nonce": 4389,
+ "data": "0x",
+ "r": "0xd0d163e1e723456684597da2dc294a609a5306dc7ed09bb2c21c5b2b35d50959",
+ "s": "0x3a1e3f9bb44f5dd04233179835855e8353c8a66404d166fec0c18896477ff681",
+ "v": 636,
+ "creates": null,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 1,
+ "chainId": 300,
+ "isL1Originated": false,
+ "status": "verified",
+ "fee": "0x1e6e69c8c080",
+ "gasPerPubdata": "0x4e20",
+ "initiatorAddress": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "receivedAt": "2023-12-19T18:01:15.030Z",
+ "ethCommitTxHash": "0x99d52709c7618c7a97e2ed29431314ada6d761a7ff34dc76b7ff9f348384c0cb",
+ "ethProveTxHash": "0x0bebce7f6471d6a4d7cdb4a22b98a64b65edf1299c815e304faec703e12c8f96",
+ "ethExecuteTxHash": "0x699234e572486fc3085b0d5b68bd04c88c3c537861d0bfdd77e63dbf6a46bd19",
+ "receiptStatus": 1
+ },
+ "transactionReceipt": {
+ "to": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "from": "0xe85B66fCA403bc35bcAE17CE7758e942aEa65Dc0",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x02afe7"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000004643d9c01f80",
+ "logIndex": 0,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 1,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 14361,
+ "transactionHash": "0x3007860374485c9de9cf1dce1e285865b63ce15558e69d99d1a0300684b8e12e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000e85b66fca403bc35bcae17ce7758e942aea65dc0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000027d56ff75f00",
+ "logIndex": 2,
+ "blockHash": "0x7c17a9583d454914d142575b0158881e6dc5ce004c8e14f3bd6a49e22a8c6ee7",
+ "l1BatchNumber": 4495
+ }
+ ],
+ "blockNumber": 14361,
+ "confirmations": 1247,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0b532b80"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 4495,
+ "l1BatchTxIndex": 1,
+ "l2ToL1Logs": [],
+ "byzantium": true
+ }
+ }
+ ],
+ "changedBalances": [
+ {
+ "address": "0xe85b66fca403bc35bcae17ce7758e942aea65dc0",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14361,
+ "balance": {
+ "type": "BigNumber",
+ "hex": "0x0a361a809ae5a815"
+ },
+ "tokenType": "ETH"
+ },
+ {
+ "address": "0x0000000000000000000000000000000000008001",
+ "tokenAddress": "0x000000000000000000000000000000000000800a",
+ "blockNumber": 14361,
+ "balance": {
+ "type": "BigNumber",
+ "hex": "0x44219a19ed80"
+ },
+ "tokenType": "ETH"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/data-fetcher/test/app.e2e-spec.ts b/packages/data-fetcher/test/app.e2e-spec.ts
new file mode 100644
index 0000000000..5ab2018457
--- /dev/null
+++ b/packages/data-fetcher/test/app.e2e-spec.ts
@@ -0,0 +1,25 @@
+import { Test, TestingModule } from "@nestjs/testing";
+import { INestApplication } from "@nestjs/common";
+import * as request from "supertest";
+import { AppModule } from "./../src/app.module";
+
+describe("AppController (e2e)", () => {
+ let app: INestApplication;
+
+ beforeEach(async () => {
+ const moduleFixture: TestingModule = await Test.createTestingModule({
+ imports: [AppModule],
+ }).compile();
+
+ app = moduleFixture.createNestApplication();
+ await app.init();
+ });
+
+ it("/health (GET)", () => {
+ return request(app.getHttpServer()).get("/health").expect(200);
+ });
+
+ it("/ready (GET)", () => {
+ return request(app.getHttpServer()).get("/ready").expect(200);
+ });
+});
diff --git a/packages/data-fetcher/test/jest-e2e.json b/packages/data-fetcher/test/jest-e2e.json
new file mode 100644
index 0000000000..e9d912f3e3
--- /dev/null
+++ b/packages/data-fetcher/test/jest-e2e.json
@@ -0,0 +1,9 @@
+{
+ "moduleFileExtensions": ["js", "json", "ts"],
+ "rootDir": ".",
+ "testEnvironment": "node",
+ "testRegex": ".e2e-spec.ts$",
+ "transform": {
+ "^.+\\.(t|j)s$": "ts-jest"
+ }
+}
diff --git a/packages/data-fetcher/test/logs/block-with-no-txs-logs.json b/packages/data-fetcher/test/logs/block-with-no-txs-logs.json
new file mode 100644
index 0000000000..fd00518101
--- /dev/null
+++ b/packages/data-fetcher/test/logs/block-with-no-txs-logs.json
@@ -0,0 +1,18 @@
+[
+ {
+ "blockNumber": 6711853,
+ "blockHash": "0xe9b0940095e6e708deebd75fa0320cad868ec5029f92cf8cc8fb464314f5141e",
+ "transactionIndex": 0,
+ "removed": false,
+ "address": "0x000000000000000000000000000000000000800A",
+ "data": "0x00000000000000000000000000000000000000000000000000f22ec29c9c4980",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000a9232040bf0e0aea2578a5b2243f2916dbfc0a69"
+ ],
+ "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 0,
+ "l1BatchNumber": 76725
+ }
+]
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/address-out-of-range.json b/packages/data-fetcher/test/transactionReceipts/address-out-of-range.json
new file mode 100644
index 0000000000..48df043517
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/address-out-of-range.json
@@ -0,0 +1,80 @@
+{
+ "to": "0x9923573104957bF457a3C4DF0e21c8b389Dd43df",
+ "from": "0xe93685f3bBA03016F02bD1828BaDD6195988D950",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x8ddf79"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "transactionHash": "0x52cdf727855ce9310b69a75d84fa23662d451e2dbcea64f3b277db12d78ab9ef",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 4985412,
+ "transactionHash": "0x52cdf727855ce9310b69a75d84fa23662d451e2dbcea64f3b277db12d78ab9ef",
+ "address": "0x39c4bDfaC23D10180e01c09BedfA0A508D4aD4D3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x00000000000000000000001476ff48d4bd33b4d5943bc82b8ee9ccaf079bcaaf",
+ "0x0000000000000000000000000000000000000000000000000000000000000015"
+ ],
+ "data": "0x",
+ "logIndex": 5,
+ "blockHash": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "l1BatchNumber": 50073
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 4985412,
+ "transactionHash": "0x52cdf727855ce9310b69a75d84fa23662d451e2dbcea64f3b277db12d78ab9ef",
+ "address": "0x6dd28C2c5B91DD63b4d4E78EcAC7139878371768",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x00000000000000000000001438686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ "0x00000000000000000000000000000000000000000000000000000000015ef3c4"
+ ],
+ "data": "0x",
+ "logIndex": 6,
+ "blockHash": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "l1BatchNumber": 50073
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 4985412,
+ "transactionHash": "0x52cdf727855ce9310b69a75d84fa23662d451e2dbcea64f3b277db12d78ab9ef",
+ "address": "0x6dd28C2c5B91DD63b4d4E78EcAC7139878371768",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000001438686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ "0x00000000000000000000001548686aa0f4e8fc2fd2910272671b26ff9c53c73a",
+ "0x00000000000000000000000000000000000000000000000000000000016ef3c4"
+ ],
+ "data": "0x",
+ "logIndex": 7,
+ "blockHash": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "l1BatchNumber": 50073
+ }
+ ],
+ "blockNumber": 4985412,
+ "confirmations": 381904,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0ee6b280"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 50073,
+ "l1BatchTxIndex": 229,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/erc20/bridge-deposit-from-l1-to-different-address.json b/packages/data-fetcher/test/transactionReceipts/erc20/bridge-deposit-from-l1-to-different-address.json
new file mode 100644
index 0000000000..02be12610a
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/erc20/bridge-deposit-from-l1-to-different-address.json
@@ -0,0 +1,103 @@
+{
+ "to": "0xc62A6e5D98b3A0DE9Ec4A930fbb354443E92E9e0",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0b4c53"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000011c37937e08000",
+ "logIndex": 0,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x000000000000000000000000c62a6e5d98b3a0de9ec4a930fbb354443e92e9e0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000011c37937e08000",
+ "logIndex": 1,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000150b5fa93bf00",
+ "logIndex": 2,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000db01bc43a500",
+ "logIndex": 3,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ }
+ ],
+ "blockNumber": 7483775,
+ "confirmations": 37064,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l1BatchNumber": 91766,
+ "l1BatchTxIndex": 741,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7483775,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
diff --git a/packages/data-fetcher/test/transactionReceipts/erc20/bridge-deposit-from-l1.json b/packages/data-fetcher/test/transactionReceipts/erc20/bridge-deposit-from-l1.json
new file mode 100644
index 0000000000..116b91743f
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/erc20/bridge-deposit-from-l1.json
@@ -0,0 +1,148 @@
+{
+ "to": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "from": "0xc0543dab6aC5D3e3fF2E5A5E39e15186d0306808",
+ "contractAddress": "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ "transactionIndex": 1,
+ "root": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0191d5"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ "topics": [
+ "0x1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e",
+ "0x0000000000000000000000004da6c020ff6492392dcd6f7943586eae42731d64"
+ ],
+ "data": "0x",
+ "logIndex": 7,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0x0000000000000000000000000000000000008006",
+ "topics": [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "0x0100014340e955cbf39159da998b3374bee8f3c0b3c75a7a9e3df6b85052379d",
+ "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571"
+ ],
+ "data": "0x",
+ "logIndex": 8,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ "topics": [
+ "0xe6b2ac4004ee4493db8844da5db69722d2128345671818c3c41928655a83fb2c",
+ "0x0000000000000000000000000db321efaa9e380d0b37b55b530cdaa62728b9a3"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000441444c3100000000000000000000000000000000000000000000000000000000",
+ "logIndex": 9,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ "topics": [
+ "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 10,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x000000000000000000000000b7e2355b87ff9ae9b146ca6dcee9c02157937b01"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000016345785d8a0000",
+ "logIndex": 11,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0xdc187378edD8Ed1585fb47549Cc5fe633295d571",
+ "topics": [
+ "0x397b33b307fc137878ebfc75b295289ec0ee25a31bb5bf034f33256fe8ea2aa6",
+ "0x000000000000000000000000b7e2355b87ff9ae9b146ca6dcee9c02157937b01"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000016345785d8a0000",
+ "logIndex": 12,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3233097,
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "address": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "topics": [
+ "0xb84fba9af218da60d299dc177abd5805e7ac541d2673cbee7808c10017874f63",
+ "0x000000000000000000000000b7e2355b87ff9ae9b146ca6dcee9c02157937b01",
+ "0x000000000000000000000000b7e2355b87ff9ae9b146ca6dcee9c02157937b01",
+ "0x000000000000000000000000dc187378edd8ed1585fb47549cc5fe633295d571"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000016345785d8a0000",
+ "logIndex": 13,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161
+ }
+ ],
+ "blockNumber": 3233097,
+ "confirmations": 1077312,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604161,
+ "l1BatchTxIndex": 71,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 3233097,
+ "blockHash": "0xdfd071dcb9c802f7d11551f4769ca67842041ffb81090c49af7f089c5823f39c",
+ "l1BatchNumber": 604161,
+ "transactionIndex": 1,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x5e018d2a81dbd1ef80ff45171dd241cb10670dcb091e324401ff8f52293841b0",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/erc20/deploy-l2.json b/packages/data-fetcher/test/transactionReceipts/erc20/deploy-l2.json
new file mode 100644
index 0000000000..e91f1c342c
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/erc20/deploy-l2.json
@@ -0,0 +1,107 @@
+{
+ "to": "0x0000000000000000000000000000000000008006",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58",
+ "transactionIndex": 0,
+ "root": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x076cd64f"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "transactionHash": "0x55f31c010dc9ae929e192cd9950027e09b647543b3d7b0f866cb74bc7941009d",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3277437,
+ "transactionHash": "0x55f31c010dc9ae929e192cd9950027e09b647543b3d7b0f866cb74bc7941009d",
+ "address": "0x0000000000000000000000000000000000008004",
+ "topics": [
+ "0xc94722ff13eacf53547c4741dab5228353a05938ffcdd5d4a2d533ae0e618287",
+ "0x01000271e819e920185598bc6fb453dedd8425e0a1eb485958a12affccdea67a",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "data": "0x",
+ "logIndex": 0,
+ "blockHash": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "l1BatchNumber": 604708
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3277437,
+ "transactionHash": "0x55f31c010dc9ae929e192cd9950027e09b647543b3d7b0f866cb74bc7941009d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000420c98159bd600",
+ "logIndex": 1,
+ "blockHash": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "l1BatchNumber": 604708
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3277437,
+ "transactionHash": "0x55f31c010dc9ae929e192cd9950027e09b647543b3d7b0f866cb74bc7941009d",
+ "address": "0xD144ca8Aa2E7DFECD56a3CCcBa1cd873c8e5db58",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000056bc75e2d63100000",
+ "logIndex": 2,
+ "blockHash": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "l1BatchNumber": 604708
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3277437,
+ "transactionHash": "0x55f31c010dc9ae929e192cd9950027e09b647543b3d7b0f866cb74bc7941009d",
+ "address": "0x0000000000000000000000000000000000008006",
+ "topics": [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x01000271e819e920185598bc6fb453dedd8425e0a1eb485958a12affccdea67a",
+ "0x000000000000000000000000d144ca8aa2e7dfecd56a3cccba1cd873c8e5db58"
+ ],
+ "data": "0x",
+ "logIndex": 3,
+ "blockHash": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "l1BatchNumber": 604708
+ }
+ ],
+ "blockNumber": 3277437,
+ "confirmations": 1032548,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 113,
+ "l1BatchNumber": 604708,
+ "l1BatchTxIndex": 23,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 3277437,
+ "blockHash": "0xb8654ce9a06fe2e6b8723177360280fc13fa8b8159fdc1504a824afe9cdbb568",
+ "l1BatchNumber": 604708,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008004",
+ "key": "0x01000271e819e920185598bc6fb453dedd8425e0a1eb485958a12affccdea67a",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "transactionHash": "0x55f31c010dc9ae929e192cd9950027e09b647543b3d7b0f866cb74bc7941009d",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/erc20/transfer.json b/packages/data-fetcher/test/transactionReceipts/erc20/transfer.json
new file mode 100644
index 0000000000..5508a283d1
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/erc20/transfer.json
@@ -0,0 +1,62 @@
+{
+ "to": "0x7AA5F26e03B12a78e3fF1C454547701443144C67",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x5ca8fb516cd361d74fe50de6172c4e676e0f79365e67212f6a098f4f4b36af14",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x2f84c2"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x5ca8fb516cd361d74fe50de6172c4e676e0f79365e67212f6a098f4f4b36af14",
+ "transactionHash": "0xbb9c4b21c235ada7be48da89ca574dfb3c1f9126f3b879060ace14e37239053d",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3615452,
+ "transactionHash": "0xbb9c4b21c235ada7be48da89ca574dfb3c1f9126f3b879060ace14e37239053d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000152d41ffe1400",
+ "logIndex": 0,
+ "blockHash": "0x5ca8fb516cd361d74fe50de6172c4e676e0f79365e67212f6a098f4f4b36af14",
+ "l1BatchNumber": 608896
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3615452,
+ "transactionHash": "0xbb9c4b21c235ada7be48da89ca574dfb3c1f9126f3b879060ace14e37239053d",
+ "address": "0x7AA5F26e03B12a78e3fF1C454547701443144C67",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000000a",
+ "logIndex": 1,
+ "blockHash": "0x5ca8fb516cd361d74fe50de6172c4e676e0f79365e67212f6a098f4f4b36af14",
+ "l1BatchNumber": 608896
+ }
+ ],
+ "blockNumber": 3615452,
+ "confirmations": 700078,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 608896,
+ "l1BatchTxIndex": 160,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/erc20/withdrawal.json b/packages/data-fetcher/test/transactionReceipts/erc20/withdrawal.json
new file mode 100644
index 0000000000..1279189ec0
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/erc20/withdrawal.json
@@ -0,0 +1,136 @@
+{
+ "to": "0x00ff932A6d70E2B8f1Eb4919e1e09C1923E7e57b",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 3,
+ "root": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x052bde"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "logs": [
+ {
+ "transactionIndex": 3,
+ "blockNumber": 7492781,
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000c51affb6ed80",
+ "logIndex": 9,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863
+ },
+ {
+ "transactionIndex": 3,
+ "blockNumber": 7492781,
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "address": "0x24A5f3f8b311B053A55C90CFfF3BD2EE34c85fC0",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000055de6a779bbac0000",
+ "logIndex": 10,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863
+ },
+ {
+ "transactionIndex": 3,
+ "blockNumber": 7492781,
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "address": "0x24A5f3f8b311B053A55C90CFfF3BD2EE34c85fC0",
+ "topics": [
+ "0x9b5b9a05e4726d8bb959f1440e05c6b8109443f2083bc4e386237d7654526553",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000055de6a779bbac0000",
+ "logIndex": 11,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863
+ },
+ {
+ "transactionIndex": 3,
+ "blockNumber": 7492781,
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "address": "0x0000000000000000000000000000000000008008",
+ "topics": [
+ "0x3a36e47291f4201faf137fab081d92295bce2d53be2c6ca68ba82c7faa9ce241",
+ "0x00000000000000000000000000ff932a6d70e2b8f1eb4919e1e09c1923e7e57b",
+ "0x28057c3d70a5cccadba3e26a81b998688608e2cb98add3b2ff5cba8d090465ff"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004c11a2ccc1c62a6e5d98b3a0de9ec4a930fbb354443e92e9e0ba9ac54b292243e798516d03735f69867188ddec0000000000000000000000000000000000000000000000055de6a779bbac00000000000000000000000000000000000000000000",
+ "logIndex": 12,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863
+ },
+ {
+ "transactionIndex": 3,
+ "blockNumber": 7492781,
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "address": "0x00ff932A6d70E2B8f1Eb4919e1e09C1923E7e57b",
+ "topics": [
+ "0x2fc3848834aac8e883a2d2a17a7514dc4f2d3dd268089df9b9f5d918259ef3b0",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x000000000000000000000000c62a6e5d98b3a0de9ec4a930fbb354443e92e9e0",
+ "0x00000000000000000000000024a5f3f8b311b053a55c90cfff3bd2ee34c85fc0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000055de6a779bbac0000",
+ "logIndex": 13,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863
+ },
+ {
+ "transactionIndex": 3,
+ "blockNumber": 7492781,
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000780bd72ca280",
+ "logIndex": 14,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863
+ }
+ ],
+ "blockNumber": 7492781,
+ "confirmations": 28273,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0ee6b280"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 91863,
+ "l1BatchTxIndex": 379,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7492781,
+ "blockHash": "0x532456dcbaf50e54c31d48e076f5c1c69830dc8c514e6b012bceb4769dd3f02c",
+ "l1BatchNumber": 91863,
+ "transactionIndex": 3,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008008",
+ "key": "0x00000000000000000000000000ff932a6d70e2b8f1eb4919e1e09c1923e7e57b",
+ "value": "0x28057c3d70a5cccadba3e26a81b998688608e2cb98add3b2ff5cba8d090465ff",
+ "transactionHash": "0x866cd72b7b677299fba830bf7a9c227e2297256dcba021b97ede26e1ab456b8e",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/deposit-no-fee.json b/packages/data-fetcher/test/transactionReceipts/eth/deposit-no-fee.json
new file mode 100644
index 0000000000..4e71b6a04b
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/deposit-no-fee.json
@@ -0,0 +1,83 @@
+{
+ "to": "0xd754Ff5e8a6f257E162F72578A4bB0493c0681d8",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0xaffcd3be150860bd92d03ff84eabda953de0001bf4f7ce81d8fa7349ee023859",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x989680"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xaffcd3be150860bd92d03ff84eabda953de0001bf4f7ce81d8fa7349ee023859",
+ "transactionHash": "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 215276,
+ "transactionHash": "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000006f05b59d3b20000",
+ "logIndex": 3,
+ "blockHash": "0xaffcd3be150860bd92d03ff84eabda953de0001bf4f7ce81d8fa7349ee023859"
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 215276,
+ "transactionHash": "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000006f05b59d3b20000",
+ "logIndex": 4,
+ "blockHash": "0xaffcd3be150860bd92d03ff84eabda953de0001bf4f7ce81d8fa7349ee023859"
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 215276,
+ "transactionHash": "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 5,
+ "blockHash": "0xaffcd3be150860bd92d03ff84eabda953de0001bf4f7ce81d8fa7349ee023859"
+ }
+ ],
+ "blockNumber": 215276,
+ "confirmations": 5,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 215276,
+ "blockHash": "0xaffcd3be150860bd92d03ff84eabda953de0001bf4f7ce81d8fa7349ee023859",
+ "transactionIndex": 1,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x7cc7cc0326af164b15de04de3b153a7a55afb14a7897298a0a84f9507d483d1d",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/deposit-to-different-address.json b/packages/data-fetcher/test/transactionReceipts/eth/deposit-to-different-address.json
new file mode 100644
index 0000000000..f2e6a4b89a
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/deposit-to-different-address.json
@@ -0,0 +1,103 @@
+{
+ "to": "0xc62A6e5D98b3A0DE9Ec4A930fbb354443E92E9e0",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0b4c53"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000011c37937e08000",
+ "logIndex": 0,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x000000000000000000000000c62a6e5d98b3a0de9ec4a930fbb354443e92e9e0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000011c37937e08000",
+ "logIndex": 1,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000150b5fa93bf00",
+ "logIndex": 2,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7483775,
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000db01bc43a500",
+ "logIndex": 3,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766
+ }
+ ],
+ "blockNumber": 7483775,
+ "confirmations": 34235,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l1BatchNumber": 91766,
+ "l1BatchTxIndex": 741,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7483775,
+ "blockHash": "0xbf9f86ca70f96a9cdbc537226dc5c1a6216eeb740169331c4cb990e957ceb25c",
+ "l1BatchNumber": 91766,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x90b20334a21b92843b5d0a2127a0fb3f9aa9661e1ead7a7a4bc27c5ce1e8584a",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/deposit-zero-value.json b/packages/data-fetcher/test/transactionReceipts/eth/deposit-zero-value.json
new file mode 100644
index 0000000000..657d996397
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/deposit-zero-value.json
@@ -0,0 +1,74 @@
+{
+ "to": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xb80764b1d38c69bf4e4e509be7522d3387bdd9f413b301fbcdeee43e23b9c06a",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x08baa6"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xb80764b1d38c69bf4e4e509be7522d3387bdd9f413b301fbcdeee43e23b9c06a",
+ "transactionHash": "0x7d7bd680763ab90fed0097cd75ae468ce01b7cfb64b4e2c74f9e47e3ba73f937",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485219,
+ "transactionHash": "0x7d7bd680763ab90fed0097cd75ae468ce01b7cfb64b4e2c74f9e47e3ba73f937",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000010425b6917e00",
+ "logIndex": 0,
+ "blockHash": "0xb80764b1d38c69bf4e4e509be7522d3387bdd9f413b301fbcdeee43e23b9c06a",
+ "l1BatchNumber": 91784
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485219,
+ "transactionHash": "0x7d7bd680763ab90fed0097cd75ae468ce01b7cfb64b4e2c74f9e47e3ba73f937",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000007c948f3acf00",
+ "logIndex": 1,
+ "blockHash": "0xb80764b1d38c69bf4e4e509be7522d3387bdd9f413b301fbcdeee43e23b9c06a",
+ "l1BatchNumber": 91784
+ }
+ ],
+ "blockNumber": 7485219,
+ "confirmations": 31890,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l1BatchNumber": 91784,
+ "l1BatchTxIndex": 182,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7485219,
+ "blockHash": "0xb80764b1d38c69bf4e4e509be7522d3387bdd9f413b301fbcdeee43e23b9c06a",
+ "l1BatchNumber": 91784,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x7d7bd680763ab90fed0097cd75ae468ce01b7cfb64b4e2c74f9e47e3ba73f937",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x7d7bd680763ab90fed0097cd75ae468ce01b7cfb64b4e2c74f9e47e3ba73f937",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/deposit.json b/packages/data-fetcher/test/transactionReceipts/eth/deposit.json
new file mode 100644
index 0000000000..a07d56d8a6
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/deposit.json
@@ -0,0 +1,103 @@
+{
+ "to": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0acb75"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002386f26fc10000",
+ "logIndex": 0,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002386f26fc10000",
+ "logIndex": 1,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000141b56ff62900",
+ "logIndex": 2,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000029eb1faec300",
+ "logIndex": 3,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ }
+ ],
+ "blockNumber": 7485644,
+ "confirmations": 30966,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l1BatchNumber": 91789,
+ "l1BatchTxIndex": 52,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7485644,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/transfer-to-zero-address.json b/packages/data-fetcher/test/transactionReceipts/eth/transfer-to-zero-address.json
new file mode 100644
index 0000000000..33a08e983e
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/transfer-to-zero-address.json
@@ -0,0 +1,62 @@
+{
+ "to": "0x0000000000000000000000000000000000000000",
+ "from": "0xD2229549F09aF28dd0C21b1ADE77F739aa8406b5",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0x7c43352334a15f3913dc0efa36752f576cf9678c5e76bd5687347013ff0919e9",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x36c132"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x7c43352334a15f3913dc0efa36752f576cf9678c5e76bd5687347013ff0919e9",
+ "transactionHash": "0x4ae2d3cf9b7e4d25b4323f7e8715521172bda4dfc88cfaf6b40c8cf80165b985",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 4144647,
+ "transactionHash": "0x4ae2d3cf9b7e4d25b4323f7e8715521172bda4dfc88cfaf6b40c8cf80165b985",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d2229549f09af28dd0c21b1ade77f739aa8406b5",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000017e3f22499e00",
+ "logIndex": 5,
+ "blockHash": "0x7c43352334a15f3913dc0efa36752f576cf9678c5e76bd5687347013ff0919e9",
+ "l1BatchNumber": 614908
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 4144647,
+ "transactionHash": "0x4ae2d3cf9b7e4d25b4323f7e8715521172bda4dfc88cfaf6b40c8cf80165b985",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d2229549f09af28dd0c21b1ade77f739aa8406b5",
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 6,
+ "blockHash": "0x7c43352334a15f3913dc0efa36752f576cf9678c5e76bd5687347013ff0919e9",
+ "l1BatchNumber": 614908
+ }
+ ],
+ "blockNumber": 4144647,
+ "confirmations": 174901,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 614908,
+ "l1BatchTxIndex": 60,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/transfer.json b/packages/data-fetcher/test/transactionReceipts/eth/transfer.json
new file mode 100644
index 0000000000..aca00fbaef
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/transfer.json
@@ -0,0 +1,62 @@
+{
+ "to": "0xc9593DC3dcAd5F3804AAa5af12a9d74D0c00e4B0",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x99e5cccae14c83d7ea6d935ea144d795e5ca1e4c246b2ee1a1ba003cb82ff3bd",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x3645f7"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x99e5cccae14c83d7ea6d935ea144d795e5ca1e4c246b2ee1a1ba003cb82ff3bd",
+ "transactionHash": "0xc697e19d80645ec37df566e1227edad4652d010e43c508bbd04efbaeb47e2c48",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226848,
+ "transactionHash": "0xc697e19d80645ec37df566e1227edad4652d010e43c508bbd04efbaeb47e2c48",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000018034d06a6900",
+ "logIndex": 0,
+ "blockHash": "0x99e5cccae14c83d7ea6d935ea144d795e5ca1e4c246b2ee1a1ba003cb82ff3bd",
+ "l1BatchNumber": 604080
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226848,
+ "transactionHash": "0xc697e19d80645ec37df566e1227edad4652d010e43c508bbd04efbaeb47e2c48",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000c9593dc3dcad5f3804aaa5af12a9d74d0c00e4b0"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002386f26fc10000",
+ "logIndex": 1,
+ "blockHash": "0x99e5cccae14c83d7ea6d935ea144d795e5ca1e4c246b2ee1a1ba003cb82ff3bd",
+ "l1BatchNumber": 604080
+ }
+ ],
+ "blockNumber": 3226848,
+ "confirmations": 1082007,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604080,
+ "l1BatchTxIndex": 17,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/withdrawal-to-different-address.json b/packages/data-fetcher/test/transactionReceipts/eth/withdrawal-to-different-address.json
new file mode 100644
index 0000000000..2ca6b8253c
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/withdrawal-to-different-address.json
@@ -0,0 +1,125 @@
+{
+ "to": "0x000000000000000000000000000000000000800A",
+ "from": "0xd754Ff5e8a6f257E162F72578A4bB0493c0681d8",
+ "contractAddress": null,
+ "transactionIndex": 7,
+ "root": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x07ee9d"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b",
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "logs": [
+ {
+ "transactionIndex": 7,
+ "blockNumber": 258521,
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000119f17fe16000",
+ "logIndex": 40,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b"
+ },
+ {
+ "transactionIndex": 7,
+ "blockNumber": 258521,
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x000000000000000000000000000000000000000000000000000000000000800a"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000429d069189e0000",
+ "logIndex": 41,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b"
+ },
+ {
+ "transactionIndex": 7,
+ "blockNumber": 258521,
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "address": "0x0000000000000000000000000000000000008008",
+ "topics": [
+ "0x3a36e47291f4201faf137fab081d92295bce2d53be2c6ca68ba82c7faa9ce241",
+ "0x000000000000000000000000000000000000000000000000000000000000800a",
+ "0xdbd13cfc2b65ffede0008bdfc0151047dcd0b2369d3db4dfd12d7fd8a8e953bd"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000386c0960f9d206eaf6819007535e893410cfa01885ce40e99a0000000000000000000000000000000000000000000000000429d069189e00000000000000000000",
+ "logIndex": 42,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b"
+ },
+ {
+ "transactionIndex": 7,
+ "blockNumber": 258521,
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b6398",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000429d069189e0000",
+ "logIndex": 43,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b"
+ },
+ {
+ "transactionIndex": 7,
+ "blockNumber": 258521,
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000002d8c82046f00",
+ "logIndex": 44,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b"
+ }
+ ],
+ "blockNumber": 258521,
+ "confirmations": 1,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x1dcd6500"
+ },
+ "status": 1,
+ "type": 0,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 258521,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b",
+ "transactionIndex": 7,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008008",
+ "key": "0x000000000000000000000000000000000000000000000000000000000000800a",
+ "value": "0xdbd13cfc2b65ffede0008bdfc0151047dcd0b2369d3db4dfd12d7fd8a8e953bd",
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "logIndex": 5
+ },
+ {
+ "blockNumber": 258521,
+ "blockHash": "0x15543afe64eaa6f26beca9cac8dd5c3465e532bd55f3402365a67eac8f62fa6b",
+ "transactionIndex": 7,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x000000000000000000000000000000000000800A",
+ "key": "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "value": "0x0000000000000000000000000000000000000000000000000429d069189e0000",
+ "transactionHash": "0x33bfd18a0aea94ba39742a9a1df595462322ecbbb25c0f767a0bf6acb41dfb2f",
+ "logIndex": 6
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/withdrawal-zero-value.json b/packages/data-fetcher/test/transactionReceipts/eth/withdrawal-zero-value.json
new file mode 100644
index 0000000000..ce8df1caa4
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/withdrawal-zero-value.json
@@ -0,0 +1,106 @@
+{
+ "to": "0x000000000000000000000000000000000000800A",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x04ac51"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "transactionHash": "0x6424dd7b1d52f9d8f6ea8300f1bf1eab65246f71f00fd3197e49c6423c9a59bf",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7508823,
+ "transactionHash": "0x6424dd7b1d52f9d8f6ea8300f1bf1eab65246f71f00fd3197e49c6423c9a59bf",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000b782effd8200",
+ "logIndex": 0,
+ "blockHash": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "l1BatchNumber": 91911
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7508823,
+ "transactionHash": "0x6424dd7b1d52f9d8f6ea8300f1bf1eab65246f71f00fd3197e49c6423c9a59bf",
+ "address": "0x0000000000000000000000000000000000008008",
+ "topics": [
+ "0x3a36e47291f4201faf137fab081d92295bce2d53be2c6ca68ba82c7faa9ce241",
+ "0x000000000000000000000000000000000000000000000000000000000000800a",
+ "0x39f8871fd3306b7a3bc2827a71f5b1344f8fa6a08d09fbcfbbb9c030566c926c"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000386c0960f9c62a6e5d98b3a0de9ec4a930fbb354443e92e9e000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 1,
+ "blockHash": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "l1BatchNumber": 91911
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7508823,
+ "transactionHash": "0x6424dd7b1d52f9d8f6ea8300f1bf1eab65246f71f00fd3197e49c6423c9a59bf",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b6398",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x000000000000000000000000c62a6e5d98b3a0de9ec4a930fbb354443e92e9e0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 2,
+ "blockHash": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "l1BatchNumber": 91911
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7508823,
+ "transactionHash": "0x6424dd7b1d52f9d8f6ea8300f1bf1eab65246f71f00fd3197e49c6423c9a59bf",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000071e06f110780",
+ "logIndex": 3,
+ "blockHash": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "l1BatchNumber": 91911
+ }
+ ],
+ "blockNumber": 7508823,
+ "confirmations": 9699,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0ee6b280"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 91911,
+ "l1BatchTxIndex": 322,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7508823,
+ "blockHash": "0x9106c0fcc2b15a1184b37da5406e4599f467e95e466fb87244e201d2e5bc3eac",
+ "l1BatchNumber": 91911,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008008",
+ "key": "0x000000000000000000000000000000000000000000000000000000000000800a",
+ "value": "0x39f8871fd3306b7a3bc2827a71f5b1344f8fa6a08d09fbcfbbb9c030566c926c",
+ "transactionHash": "0x6424dd7b1d52f9d8f6ea8300f1bf1eab65246f71f00fd3197e49c6423c9a59bf",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
diff --git a/packages/data-fetcher/test/transactionReceipts/eth/withdrawal.json b/packages/data-fetcher/test/transactionReceipts/eth/withdrawal.json
new file mode 100644
index 0000000000..275f924e5b
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/eth/withdrawal.json
@@ -0,0 +1,125 @@
+{
+ "to": "0x000000000000000000000000000000000000800A",
+ "from": "0xd754Ff5e8a6f257E162F72578A4bB0493c0681d8",
+ "contractAddress": null,
+ "transactionIndex": 5,
+ "root": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x07c4a9"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35",
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "logs": [
+ {
+ "transactionIndex": 5,
+ "blockNumber": 264367,
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000119f17fe16000",
+ "logIndex": 19,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35"
+ },
+ {
+ "transactionIndex": 5,
+ "blockNumber": 264367,
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x000000000000000000000000000000000000000000000000000000000000800a"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000016345785d8a0000",
+ "logIndex": 20,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35"
+ },
+ {
+ "transactionIndex": 5,
+ "blockNumber": 264367,
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "address": "0x0000000000000000000000000000000000008008",
+ "topics": [
+ "0x3a36e47291f4201faf137fab081d92295bce2d53be2c6ca68ba82c7faa9ce241",
+ "0x000000000000000000000000000000000000000000000000000000000000800a",
+ "0x4289f204436c7a9c0c7ed3c0ebdecba4677e9058363358a9b794d3a4eeb27ef4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000386c0960f9d754ff5e8a6f257e162f72578a4bb0493c0681d8000000000000000000000000000000000000000000000000016345785d8a00000000000000000000",
+ "logIndex": 21,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35"
+ },
+ {
+ "transactionIndex": 5,
+ "blockNumber": 264367,
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b6398",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000016345785d8a0000",
+ "logIndex": 22,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35"
+ },
+ {
+ "transactionIndex": 5,
+ "blockNumber": 264367,
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000326ecef5b300",
+ "logIndex": 23,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35"
+ }
+ ],
+ "blockNumber": 264367,
+ "confirmations": 1,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x1dcd6500"
+ },
+ "status": 1,
+ "type": 0,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 264367,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35",
+ "transactionIndex": 5,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008008",
+ "key": "0x000000000000000000000000000000000000000000000000000000000000800a",
+ "value": "0x4289f204436c7a9c0c7ed3c0ebdecba4677e9058363358a9b794d3a4eeb27ef4",
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "logIndex": 0
+ },
+ {
+ "blockNumber": 264367,
+ "blockHash": "0x11ccedc5cece17c7d2406a56ba0b1b60212eb0d4fef84d594f74d5c04e378e35",
+ "transactionIndex": 5,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x000000000000000000000000000000000000800A",
+ "key": "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "value": "0x000000000000000000000000000000000000000000000000016345785d8a0000",
+ "transactionHash": "0x69d3dcb5822096bab259dbba2a1b42bdfd6d1e5c4169196893cf1998ab2ca85f",
+ "logIndex": 1
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/failedTx/erc20-transfer-to-zero-address.json b/packages/data-fetcher/test/transactionReceipts/failedTx/erc20-transfer-to-zero-address.json
new file mode 100644
index 0000000000..8179402bbd
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/failedTx/erc20-transfer-to-zero-address.json
@@ -0,0 +1,47 @@
+{
+ "to": "0x7AA5F26e03B12a78e3fF1C454547701443144C67",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0x80aa7e421a4df2f949ed263d3e953b0f7180fc3836006c0a72ca31c4de452f13",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x23e8a3"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x80aa7e421a4df2f949ed263d3e953b0f7180fc3836006c0a72ca31c4de452f13",
+ "transactionHash": "0x2b9153e896c0d2651d2826e8e1a447c5e56a3e060ae7d60c2c0bfbcd966e4880",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3622224,
+ "transactionHash": "0x2b9153e896c0d2651d2826e8e1a447c5e56a3e060ae7d60c2c0bfbcd966e4880",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000001606ddfd9b8000",
+ "logIndex": 5,
+ "blockHash": "0x80aa7e421a4df2f949ed263d3e953b0f7180fc3836006c0a72ca31c4de452f13",
+ "l1BatchNumber": 609014
+ }
+ ],
+ "blockNumber": 3622224,
+ "confirmations": 692066,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 0,
+ "type": 0,
+ "l1BatchNumber": 609014,
+ "l1BatchTxIndex": 49,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/fee-with-no-deposits.json b/packages/data-fetcher/test/transactionReceipts/fee-with-no-deposits.json
new file mode 100644
index 0000000000..2cc5eab2bc
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/fee-with-no-deposits.json
@@ -0,0 +1,61 @@
+{
+ "to": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0acb75"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000141b56ff62900",
+ "logIndex": 2,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ }
+ ],
+ "blockNumber": 7485644,
+ "confirmations": 30966,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l1BatchNumber": 91789,
+ "l1BatchTxIndex": 52,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7485644,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+ }
+
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/greeter/deploy-to-l2.json b/packages/data-fetcher/test/transactionReceipts/greeter/deploy-to-l2.json
new file mode 100644
index 0000000000..0123fcbed9
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/greeter/deploy-to-l2.json
@@ -0,0 +1,63 @@
+{
+ "to": "0x0000000000000000000000000000000000008006",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": "0x27E5985dbe48d98e5fd4DAae24c4549c5CE35EC5",
+ "transactionIndex": 1,
+ "root": "0x4eeabe3247f5255f02f0485e1a4b34ad12b27ef372302afa5d5c562dc092c8f5",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x3cb6ca"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x4eeabe3247f5255f02f0485e1a4b34ad12b27ef372302afa5d5c562dc092c8f5",
+ "transactionHash": "0x7ec71b1d5369b830af3f7af4b1ef0f04e62cc3775b1c090434a93493d1b68632",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3230131,
+ "transactionHash": "0x7ec71b1d5369b830af3f7af4b1ef0f04e62cc3775b1c090434a93493d1b68632",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000001baa818335500",
+ "logIndex": 2,
+ "blockHash": "0x4eeabe3247f5255f02f0485e1a4b34ad12b27ef372302afa5d5c562dc092c8f5",
+ "l1BatchNumber": 604120
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3230131,
+ "transactionHash": "0x7ec71b1d5369b830af3f7af4b1ef0f04e62cc3775b1c090434a93493d1b68632",
+ "address": "0x0000000000000000000000000000000000008006",
+ "topics": [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x010000e3577bb87a4d3d800673b00f1293ec88e02b691551c641db915897655e",
+ "0x00000000000000000000000027e5985dbe48d98e5fd4daae24c4549c5ce35ec5"
+ ],
+ "data": "0x",
+ "logIndex": 3,
+ "blockHash": "0x4eeabe3247f5255f02f0485e1a4b34ad12b27ef372302afa5d5c562dc092c8f5",
+ "l1BatchNumber": 604120
+ }
+ ],
+ "blockNumber": 3230131,
+ "confirmations": 1079242,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 113,
+ "l1BatchNumber": 604120,
+ "l1BatchTxIndex": 43,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/greeter/exec-set-greeting.json b/packages/data-fetcher/test/transactionReceipts/greeter/exec-set-greeting.json
new file mode 100644
index 0000000000..f85485c891
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/greeter/exec-set-greeting.json
@@ -0,0 +1,47 @@
+{
+ "to": "0x27E5985dbe48d98e5fd4DAae24c4549c5CE35EC5",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0xe4b10f05a8e730bb58bbdd87bf8f9a39d751919d0248353b06c0c5981e5e1e85",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x2d337d"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xe4b10f05a8e730bb58bbdd87bf8f9a39d751919d0248353b06c0c5981e5e1e85",
+ "transactionHash": "0xaa4f89b2adc9ae0fc50d058ebc7d75ddd54b0d83c307474b09989def4a0f2fbe",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 3230534,
+ "transactionHash": "0xaa4f89b2adc9ae0fc50d058ebc7d75ddd54b0d83c307474b09989def4a0f2fbe",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000134b578f8b200",
+ "logIndex": 2,
+ "blockHash": "0xe4b10f05a8e730bb58bbdd87bf8f9a39d751919d0248353b06c0c5981e5e1e85",
+ "l1BatchNumber": 604128
+ }
+ ],
+ "blockNumber": 3230534,
+ "confirmations": 1079132,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604128,
+ "l1BatchTxIndex": 14,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/log-parsing-error.json b/packages/data-fetcher/test/transactionReceipts/log-parsing-error.json
new file mode 100644
index 0000000000..e395f0298f
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/log-parsing-error.json
@@ -0,0 +1,48 @@
+{
+ "to": "0x9923573104957bF457a3C4DF0e21c8b389Dd43df",
+ "from": "0xe93685f3bBA03016F02bD1828BaDD6195988D950",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x8ddf79"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "transactionHash": "0x52cdf727855ce9310b69a75d84fa23662d451e2dbcea64f3b277db12d78ab9ef",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 4985412,
+ "transactionHash": "0x52cdf727855ce9310b69a75d84fa23662d451e2dbcea64f3b277db12d78ab9ef",
+ "address": "0x39c4bDfaC23D10180e01c09BedfA0A508D4aD4D3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "",
+ "0x0000000000000000000000000000000000000000000000000000000000000015"
+ ],
+ "data": "0x",
+ "logIndex": 5,
+ "blockHash": "0x117fdf2ca53893a321bb64f015d6e1cc3a877c6f6ad6cd02ea3c71c1b9533c7f",
+ "l1BatchNumber": 50073
+ }
+ ],
+ "blockNumber": 4985412,
+ "confirmations": 381904,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x0ee6b280"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 50073,
+ "l1BatchTxIndex": 229,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/multiTransfer/eth-usdc-erc20-through-paymaster.json b/packages/data-fetcher/test/transactionReceipts/multiTransfer/eth-usdc-erc20-through-paymaster.json
new file mode 100644
index 0000000000..44a9c0eadd
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/multiTransfer/eth-usdc-erc20-through-paymaster.json
@@ -0,0 +1,347 @@
+{
+ "to": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 2,
+ "root": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0xabbeae"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "logs": [
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 3,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 4,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 5,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 6,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 7,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000000a",
+ "logIndex": 8,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x1aa41d2ee6bd6ecd7b05eb58f605dca2853bfe4b18bb145f5b93e9994b001452",
+ "0x000000000000000000000000000000000000000000000000000000000000000a",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x",
+ "logIndex": 9,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x06933fee4464766b76e036a832581d6d58101114963fbd144676a93dd343166c",
+ "0x0000000000000000000000002baec5bca9f2052489ed30668f27ab4466f0bcb3",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x00000172ebad6ddc73c86d67c5faa71c245689c10795023fffa6987e39db0feb",
+ "logIndex": 10,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x279e5597bd54fc6a839e079e8fe1cf390d8434cd908e57ef2f37bc3e2d1c5bdd",
+ "0x0000000000000000000000002baec5bca9f2052489ed30668f27ab4466f0bcb3",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000004f",
+ "logIndex": 11,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x06933fee4464766b76e036a832581d6d58101114963fbd144676a93dd343166c",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000010b075f6",
+ "logIndex": 12,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x279e5597bd54fc6a839e079e8fe1cf390d8434cd908e57ef2f37bc3e2d1c5bdd",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000009184e729ff6",
+ "logIndex": 13,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 14,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000004a",
+ "logIndex": 15,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 16,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000009184e729ff1",
+ "logIndex": 17,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 18,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 19,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000045",
+ "logIndex": 20,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 21,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000009184e729fec",
+ "logIndex": 22,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ },
+ {
+ "transactionIndex": 2,
+ "blockNumber": 3619009,
+ "transactionHash": "0xeae0368e1457fa55da486ffc772cc654d3d5b95faa220fa971ff73077fccd370",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 23,
+ "blockHash": "0x8200e149eb70a48a5e9cf4fa835d01f0d11a548ed7ca7dffe030de9ffa009db9",
+ "l1BatchNumber": 608948
+ }
+ ],
+ "blockNumber": 3619009,
+ "confirmations": 692239,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 113,
+ "l1BatchNumber": 608948,
+ "l1BatchTxIndex": 21,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/multiTransfer/eth-usdc-erc20-transfer.json b/packages/data-fetcher/test/transactionReceipts/multiTransfer/eth-usdc-erc20-transfer.json
new file mode 100644
index 0000000000..59ed7e7cee
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/multiTransfer/eth-usdc-erc20-transfer.json
@@ -0,0 +1,287 @@
+{
+ "to": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x8e272f"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000004a061c5f0b100",
+ "logIndex": 0,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000000a",
+ "logIndex": 1,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x1aa41d2ee6bd6ecd7b05eb58f605dca2853bfe4b18bb145f5b93e9994b001452",
+ "0x000000000000000000000000000000000000000000000000000000000000000a",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x",
+ "logIndex": 2,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x06933fee4464766b76e036a832581d6d58101114963fbd144676a93dd343166c",
+ "0x0000000000000000000000002baec5bca9f2052489ed30668f27ab4466f0bcb3",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x00000172ebad6ddc73c86d67c5faa71c245689c10795023fffb1c56e729fadf5",
+ "logIndex": 3,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x279e5597bd54fc6a839e079e8fe1cf390d8434cd908e57ef2f37bc3e2d1c5bdd",
+ "0x0000000000000000000000002baec5bca9f2052489ed30668f27ab4466f0bcb3",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000059",
+ "logIndex": 4,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x06933fee4464766b76e036a832581d6d58101114963fbd144676a93dd343166c",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000010b07600",
+ "logIndex": 5,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x08b222F412EB5D141fB32Db443f2eeD06ae65a24",
+ "topics": [
+ "0x279e5597bd54fc6a839e079e8fe1cf390d8434cd908e57ef2f37bc3e2d1c5bdd",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91",
+ "0x000000000000000000000000000000000000000000000000000000000000000a"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000009184e72a000",
+ "logIndex": 6,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 7,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000054",
+ "logIndex": 8,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 9,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000009184e729ffb",
+ "logIndex": 10,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 11,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 12,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000000000000004f",
+ "logIndex": 13,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 14,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000008b222f412eb5d141fb32db443f2eed06ae65a24"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000009184e729ff6",
+ "logIndex": 15,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3618694,
+ "transactionHash": "0x9a9218b64947ffc0e7993c1dd2cbc3377b33c0773a445662e8c833ed17369cf3",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000005",
+ "logIndex": 16,
+ "blockHash": "0x922e13d64aa56d4ec660d1e16f8d1805a3640b2f6629369a5f5f26bd41aa2cc5",
+ "l1BatchNumber": 608942
+ }
+ ],
+ "blockNumber": 3618694,
+ "confirmations": 692176,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 608942,
+ "l1BatchTxIndex": 88,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/multiTransfer/multi-eth-transfer.json b/packages/data-fetcher/test/transactionReceipts/multiTransfer/multi-eth-transfer.json
new file mode 100644
index 0000000000..3a427cb982
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/multiTransfer/multi-eth-transfer.json
@@ -0,0 +1,152 @@
+{
+ "to": "0xbBA0cf82ce98acd455bd34d7a53Bb565A31372A6",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x7612a6"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 0,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 1,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 2,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 3,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 4,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000bba0cf82ce98acd455bd34d7a53bb565a31372a6"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002386f26fc10000",
+ "logIndex": 5,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000bba0cf82ce98acd455bd34d7a53bb565a31372a6",
+ "0x00000000000000000000000065ebd487e692d688f2a36fb833729076dc85ed34"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000011c37937e08000",
+ "logIndex": 6,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3601749,
+ "transactionHash": "0x8ed4aa94bb4a28ce174e435d60297d765885dff83a543b49ad57adedec99cfe3",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000bba0cf82ce98acd455bd34d7a53bb565a31372a6",
+ "0x000000000000000000000000c9593dc3dcad5f3804aaa5af12a9d74d0c00e4b0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000011c37937e08000",
+ "logIndex": 7,
+ "blockHash": "0x52d9ac786a15334b856ba8b8fb4ff0dbff0b68d52363ab4b52f81165adf9e4d9",
+ "l1BatchNumber": 608725
+ }
+ ],
+ "blockNumber": 3601749,
+ "confirmations": 709764,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 113,
+ "l1BatchNumber": 608725,
+ "l1BatchTxIndex": 97,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/nestedContractsCalls/sub-calls-to-other-contracts.json b/packages/data-fetcher/test/transactionReceipts/nestedContractsCalls/sub-calls-to-other-contracts.json
new file mode 100644
index 0000000000..d912f2d5bc
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/nestedContractsCalls/sub-calls-to-other-contracts.json
@@ -0,0 +1,91 @@
+{
+ "to": "0x6385da58bda1281D3E021d09ae2E273A2F6a3320",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xfae194557e3e8ec559c5125cf284c7c60e66cb3d82b8b71d7be16115ef6e3a6c",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x4e21a8"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xfae194557e3e8ec559c5125cf284c7c60e66cb3d82b8b71d7be16115ef6e3a6c",
+ "transactionHash": "0x8a7a99459d2278c83a8450cc36c0e5b75bee250a60e4b66dea182325afd8fa07",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3680310,
+ "transactionHash": "0x8a7a99459d2278c83a8450cc36c0e5b75bee250a60e4b66dea182325afd8fa07",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000220aed39b7400",
+ "logIndex": 0,
+ "blockHash": "0xfae194557e3e8ec559c5125cf284c7c60e66cb3d82b8b71d7be16115ef6e3a6c",
+ "l1BatchNumber": 609834
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3680310,
+ "transactionHash": "0x8a7a99459d2278c83a8450cc36c0e5b75bee250a60e4b66dea182325afd8fa07",
+ "address": "0x775e71AdDb6b6c4Ed78044953940A1da579C0AC3",
+ "topics": [
+ "0x3a5e248acc8913ccb0680211c01d6d63a570cf10cc8e5b88555b96c292b1a6cd",
+ "0x00000000000000000000000025e691d349c01532ac3f57efff772dc8a0ccd76f",
+ "0x0000000000000000000000000000000000000000000000000000000000000002"
+ ],
+ "data": "0x",
+ "logIndex": 1,
+ "blockHash": "0xfae194557e3e8ec559c5125cf284c7c60e66cb3d82b8b71d7be16115ef6e3a6c",
+ "l1BatchNumber": 609834
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3680310,
+ "transactionHash": "0x8a7a99459d2278c83a8450cc36c0e5b75bee250a60e4b66dea182325afd8fa07",
+ "address": "0x25E691D349C01532ac3f57efFF772DC8a0CcD76f",
+ "topics": [
+ "0x5b5c76835946441a5f2c68c77c14080e7c193f9fde78b5cce15e5fa002d66241",
+ "0xf034171380d959bed990c45ddc4db006b764efef7708f431165443d33b6e5b5b",
+ "0x0000000000000000000000000000000000000000000000000000000000000002"
+ ],
+ "data": "0x",
+ "logIndex": 2,
+ "blockHash": "0xfae194557e3e8ec559c5125cf284c7c60e66cb3d82b8b71d7be16115ef6e3a6c",
+ "l1BatchNumber": 609834
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3680310,
+ "transactionHash": "0x8a7a99459d2278c83a8450cc36c0e5b75bee250a60e4b66dea182325afd8fa07",
+ "address": "0x6385da58bda1281D3E021d09ae2E273A2F6a3320",
+ "topics": [
+ "0xad342ef2af75836b39edaec7343056d1abf8fa75da78bf7d6d29761c5491259c",
+ "0xb7d91633749c02f5f37d97488342efccff59400444ac7a7e827d27b883046248"
+ ],
+ "data": "0x",
+ "logIndex": 3,
+ "blockHash": "0xfae194557e3e8ec559c5125cf284c7c60e66cb3d82b8b71d7be16115ef6e3a6c",
+ "l1BatchNumber": 609834
+ }
+ ],
+ "blockNumber": 3680310,
+ "confirmations": 635044,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 609834,
+ "l1BatchTxIndex": 51,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/nft/approve.json b/packages/data-fetcher/test/transactionReceipts/nft/approve.json
new file mode 100644
index 0000000000..fe23332643
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/nft/approve.json
@@ -0,0 +1,63 @@
+{
+ "to": "0x89bcb56033920b8a654109fAEB1f87E0c3358cAD",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x2c0cbabe7c055c13cbdea88cbae8bbce838f94752cf32ae3ca461a7801b74a63",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x29b8b9"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x2c0cbabe7c055c13cbdea88cbae8bbce838f94752cf32ae3ca461a7801b74a63",
+ "transactionHash": "0x4833645ed34e16c9e2ce4d26fee2d730202ab3e76c0cd155557e7bb9344990be",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3459441,
+ "transactionHash": "0x4833645ed34e16c9e2ce4d26fee2d730202ab3e76c0cd155557e7bb9344990be",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000011f05a0378000",
+ "logIndex": 0,
+ "blockHash": "0x2c0cbabe7c055c13cbdea88cbae8bbce838f94752cf32ae3ca461a7801b74a63",
+ "l1BatchNumber": 606984
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3459441,
+ "transactionHash": "0x4833645ed34e16c9e2ce4d26fee2d730202ab3e76c0cd155557e7bb9344990be",
+ "address": "0x89bcb56033920b8a654109fAEB1f87E0c3358cAD",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "data": "0x",
+ "logIndex": 1,
+ "blockHash": "0x2c0cbabe7c055c13cbdea88cbae8bbce838f94752cf32ae3ca461a7801b74a63",
+ "l1BatchNumber": 606984
+ }
+ ],
+ "blockNumber": 3459441,
+ "confirmations": 856531,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 606984,
+ "l1BatchTxIndex": 107,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/nft/deploy-l2.json b/packages/data-fetcher/test/transactionReceipts/nft/deploy-l2.json
new file mode 100644
index 0000000000..d76cf15457
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/nft/deploy-l2.json
@@ -0,0 +1,107 @@
+{
+ "to": "0x0000000000000000000000000000000000008006",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": "0x89bcb56033920b8a654109fAEB1f87E0c3358cAD",
+ "transactionIndex": 0,
+ "root": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0c5f1b1b"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "transactionHash": "0xc36a1b2d19085fbeff652e618a1e61d6d386f92cbe51373eac60077bb128b7cb",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3455042,
+ "transactionHash": "0xc36a1b2d19085fbeff652e618a1e61d6d386f92cbe51373eac60077bb128b7cb",
+ "address": "0x0000000000000000000000000000000000008004",
+ "topics": [
+ "0xc94722ff13eacf53547c4741dab5228353a05938ffcdd5d4a2d533ae0e618287",
+ "0x010004295ad252cbb8dd3a07b0d5370ff101c4de3dc17b4e839dc26263b5452c",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "data": "0x",
+ "logIndex": 0,
+ "blockHash": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "l1BatchNumber": 606932
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3455042,
+ "transactionHash": "0xc36a1b2d19085fbeff652e618a1e61d6d386f92cbe51373eac60077bb128b7cb",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000006e45eb3828c800",
+ "logIndex": 1,
+ "blockHash": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "l1BatchNumber": 606932
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3455042,
+ "transactionHash": "0xc36a1b2d19085fbeff652e618a1e61d6d386f92cbe51373eac60077bb128b7cb",
+ "address": "0x89bcb56033920b8a654109fAEB1f87E0c3358cAD",
+ "topics": [
+ "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a"
+ ],
+ "data": "0x",
+ "logIndex": 2,
+ "blockHash": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "l1BatchNumber": 606932
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3455042,
+ "transactionHash": "0xc36a1b2d19085fbeff652e618a1e61d6d386f92cbe51373eac60077bb128b7cb",
+ "address": "0x0000000000000000000000000000000000008006",
+ "topics": [
+ "0x290afdae231a3fc0bbae8b1af63698b0a1d79b21ad17df0342dfb952fe74f8e5",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x010004295ad252cbb8dd3a07b0d5370ff101c4de3dc17b4e839dc26263b5452c",
+ "0x00000000000000000000000089bcb56033920b8a654109faeb1f87e0c3358cad"
+ ],
+ "data": "0x",
+ "logIndex": 3,
+ "blockHash": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "l1BatchNumber": 606932
+ }
+ ],
+ "blockNumber": 3455042,
+ "confirmations": 860658,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 113,
+ "l1BatchNumber": 606932,
+ "l1BatchTxIndex": 15,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 3455042,
+ "blockHash": "0xac6a3a28ebd0ca9f144f52f4dc2339bc8693203a7711a80673ae3591e481a7d5",
+ "l1BatchNumber": 606932,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008004",
+ "key": "0x010004295ad252cbb8dd3a07b0d5370ff101c4de3dc17b4e839dc26263b5452c",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "transactionHash": "0xc36a1b2d19085fbeff652e618a1e61d6d386f92cbe51373eac60077bb128b7cb",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/nft/mint.json b/packages/data-fetcher/test/transactionReceipts/nft/mint.json
new file mode 100644
index 0000000000..4700513edd
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/nft/mint.json
@@ -0,0 +1,63 @@
+{
+ "to": "0x4A80888F58D004c5ef2013d2Cf974f00f42DD934",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x16b7cf22713896b1101f7918f731a2c984121c59eddb796d59a90ee40320faba",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x4ca340"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x16b7cf22713896b1101f7918f731a2c984121c59eddb796d59a90ee40320faba",
+ "transactionHash": "0x30e3abe6ac3a1b47d961213e1b1302377786f5cd537a6cd34dd3cd6473a319d0",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3339976,
+ "transactionHash": "0x30e3abe6ac3a1b47d961213e1b1302377786f5cd537a6cd34dd3cd6473a319d0",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000002570592a28a00",
+ "logIndex": 0,
+ "blockHash": "0x16b7cf22713896b1101f7918f731a2c984121c59eddb796d59a90ee40320faba",
+ "l1BatchNumber": 605512
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3339976,
+ "transactionHash": "0x30e3abe6ac3a1b47d961213e1b1302377786f5cd537a6cd34dd3cd6473a319d0",
+ "address": "0x4A80888F58D004c5ef2013d2Cf974f00f42DD934",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "data": "0x",
+ "logIndex": 1,
+ "blockHash": "0x16b7cf22713896b1101f7918f731a2c984121c59eddb796d59a90ee40320faba",
+ "l1BatchNumber": 605512
+ }
+ ],
+ "blockNumber": 3339976,
+ "confirmations": 973290,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 605512,
+ "l1BatchTxIndex": 55,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/nft/transfer.json b/packages/data-fetcher/test/transactionReceipts/nft/transfer.json
new file mode 100644
index 0000000000..c445059f2a
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/nft/transfer.json
@@ -0,0 +1,63 @@
+{
+ "to": "0x89bcb56033920b8a654109fAEB1f87E0c3358cAD",
+ "from": "0xD206eaF6819007535e893410cfa01885Ce40E99a",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xfe02bd556b7abf14d1c92e823ed5b3b8d5067f94115531301f6ac5ebb7488a7e",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x3b3fc1"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xfe02bd556b7abf14d1c92e823ed5b3b8d5067f94115531301f6ac5ebb7488a7e",
+ "transactionHash": "0x6bedb809e97f58d987aea7aad4fcfa8a3f5ecc3cde9a97093b2f3a1a170692a0",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3459471,
+ "transactionHash": "0x6bedb809e97f58d987aea7aad4fcfa8a3f5ecc3cde9a97093b2f3a1a170692a0",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000001bbaabf001a00",
+ "logIndex": 0,
+ "blockHash": "0xfe02bd556b7abf14d1c92e823ed5b3b8d5067f94115531301f6ac5ebb7488a7e",
+ "l1BatchNumber": 606985
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3459471,
+ "transactionHash": "0x6bedb809e97f58d987aea7aad4fcfa8a3f5ecc3cde9a97093b2f3a1a170692a0",
+ "address": "0x89bcb56033920b8a654109fAEB1f87E0c3358cAD",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000d206eaf6819007535e893410cfa01885ce40e99a",
+ "0x000000000000000000000000d754ff5e8a6f257e162f72578a4bb0493c0681d8",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "data": "0x",
+ "logIndex": 1,
+ "blockHash": "0xfe02bd556b7abf14d1c92e823ed5b3b8d5067f94115531301f6ac5ebb7488a7e",
+ "l1BatchNumber": 606985
+ }
+ ],
+ "blockNumber": 3459471,
+ "confirmations": 856527,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 606985,
+ "l1BatchTxIndex": 26,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/no-deposit-after-fee.json b/packages/data-fetcher/test/transactionReceipts/no-deposit-after-fee.json
new file mode 100644
index 0000000000..e6c86b32a2
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/no-deposit-after-fee.json
@@ -0,0 +1,90 @@
+{
+ "to": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "from": "0xFb7E0856e44Eff812A44A9f47733d7d55c39Aa28",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x0acb75"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002386f26fc10000",
+ "logIndex": 0,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28",
+ "0x000000000000000000000000fb7e0856e44eff812a44a9f47733d7d55c39aa28"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000002386f26fc10000",
+ "logIndex": 1,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 7485644,
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000141b56ff62900",
+ "logIndex": 2,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789
+ }
+ ],
+ "blockNumber": 7485644,
+ "confirmations": 30966,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 255,
+ "l1BatchNumber": 91789,
+ "l1BatchTxIndex": 52,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 7485644,
+ "blockHash": "0xefba20d635e8dc3c936eaa6eb4c519c38b8babcdf28b6be90e9a9a5ce841c6f7",
+ "l1BatchNumber": 91789,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0xad909404d4390c350281c9e896cfadc528d071cb87c62f4ed026016fd4694d77",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+ }
+
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/no-matching-handlers.json b/packages/data-fetcher/test/transactionReceipts/no-matching-handlers.json
new file mode 100644
index 0000000000..6bf28ea96f
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/no-matching-handlers.json
@@ -0,0 +1,91 @@
+{
+ "to": "0x6267080B2265A09371cc2763756dbaCDaF09856E",
+ "from": "0x088282B61A8CC1014186076698E35bCC92e88b0D",
+ "contractAddress": null,
+ "transactionIndex": 1,
+ "root": "0xec97ff411cc4e2ddc3d51e641d5445c106e6f062d2ad962b0bfa509a1aa36744",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x040e6b"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xec97ff411cc4e2ddc3d51e641d5445c106e6f062d2ad962b0bfa509a1aa36744",
+ "transactionHash": "0xb12a02697dd2a0e414cb8f9436c6fb7a8eb82eb403e256644235a0c618ef508d",
+ "logs": [
+ {
+ "transactionIndex": 1,
+ "blockNumber": 166322,
+ "transactionHash": "0xb12a02697dd2a0e414cb8f9436c6fb7a8eb82eb403e256644235a0c618ef508d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000088282b61a8cc1014186076698e35bcc92e88b0d",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000cdab63f37000",
+ "logIndex": 3,
+ "blockHash": "0xec97ff411cc4e2ddc3d51e641d5445c106e6f062d2ad962b0bfa509a1aa36744",
+ "l1BatchNumber": 1524
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 166322,
+ "transactionHash": "0xb12a02697dd2a0e414cb8f9436c6fb7a8eb82eb403e256644235a0c618ef508d",
+ "address": "0x6267080B2265A09371cc2763756dbaCDaF09856E",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x0000000000000000000000007869cd51c2483169e1ce06fa6912e7d1e3bf629b"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 4,
+ "blockHash": "0xec97ff411cc4e2ddc3d51e641d5445c106e6f062d2ad962b0bfa509a1aa36744",
+ "l1BatchNumber": 1524
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 166322,
+ "transactionHash": "0xb12a02697dd2a0e414cb8f9436c6fb7a8eb82eb403e256644235a0c618ef508d",
+ "address": "0x6267080B2265A09371cc2763756dbaCDaF09856E",
+ "topics": [
+ "0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885",
+ "0x0000000000000000000000007869cd51c2483169e1ce06fa6912e7d1e3bf629b"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "logIndex": 5,
+ "blockHash": "0xec97ff411cc4e2ddc3d51e641d5445c106e6f062d2ad962b0bfa509a1aa36744",
+ "l1BatchNumber": 1524
+ },
+ {
+ "transactionIndex": 1,
+ "blockNumber": 166322,
+ "transactionHash": "0xb12a02697dd2a0e414cb8f9436c6fb7a8eb82eb403e256644235a0c618ef508d",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000008001",
+ "0x000000000000000000000000088282b61a8cc1014186076698e35bcc92e88b0d"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000054c81f943900",
+ "logIndex": 6,
+ "blockHash": "0xec97ff411cc4e2ddc3d51e641d5445c106e6f062d2ad962b0bfa509a1aa36744",
+ "l1BatchNumber": 1524
+ }
+ ],
+ "blockNumber": 166322,
+ "confirmations": 649914,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x1dcd6500"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 1524,
+ "l1BatchTxIndex": 21,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/paymasters/transfer.json b/packages/data-fetcher/test/transactionReceipts/paymasters/transfer.json
new file mode 100644
index 0000000000..902fe98f19
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/paymasters/transfer.json
@@ -0,0 +1,107 @@
+{
+ "to": "0x27E5985dbe48d98e5fd4DAae24c4549c5CE35EC5",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x473da0"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "transactionHash": "0x191c7e02d6a78b6da6116ea8347f3560627caa4b7fbf766f96eccbd09bacc433",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3286895,
+ "transactionHash": "0x191c7e02d6a78b6da6116ea8347f3560627caa4b7fbf766f96eccbd09bacc433",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 0,
+ "blockHash": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "l1BatchNumber": 604854
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3286895,
+ "transactionHash": "0x191c7e02d6a78b6da6116ea8347f3560627caa4b7fbf766f96eccbd09bacc433",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 1,
+ "blockHash": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "l1BatchNumber": 604854
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3286895,
+ "transactionHash": "0x191c7e02d6a78b6da6116ea8347f3560627caa4b7fbf766f96eccbd09bacc433",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": 2,
+ "blockHash": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "l1BatchNumber": 604854
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3286895,
+ "transactionHash": "0x191c7e02d6a78b6da6116ea8347f3560627caa4b7fbf766f96eccbd09bacc433",
+ "address": "0x2Baec5bcA9f2052489Ed30668F27aB4466f0BcB3",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 3,
+ "blockHash": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "l1BatchNumber": 604854
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3286895,
+ "transactionHash": "0x191c7e02d6a78b6da6116ea8347f3560627caa4b7fbf766f96eccbd09bacc433",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000265d9a5af8af5fe070933e5e549d8fef08e09f4",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000b2cf038c49e00",
+ "logIndex": 4,
+ "blockHash": "0x3e14b6b398c8d2c2cde67103122062d676566fcf64842679296583d4e9fbb37d",
+ "l1BatchNumber": 604854
+ }
+ ],
+ "blockNumber": 3286895,
+ "confirmations": 1027156,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 113,
+ "l1BatchNumber": 604854,
+ "l1BatchTxIndex": 57,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/deposit.json b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/deposit.json
new file mode 100644
index 0000000000..84fe9fe988
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/deposit.json
@@ -0,0 +1,91 @@
+{
+ "to": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "from": "0xc0543dab6aC5D3e3fF2E5A5E39e15186d0306808",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xb7feda15d99e8d82e7d4acc06119b8bb6274a8e390582c3546ca2750d8a345e1",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x88b5"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xb7feda15d99e8d82e7d4acc06119b8bb6274a8e390582c3546ca2750d8a345e1",
+ "transactionHash": "0x25f9bd9de8260b53e2765cddb6aaafce5256fca647434c72559f0a0fb77bd715",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3221037,
+ "transactionHash": "0x25f9bd9de8260b53e2765cddb6aaafce5256fca647434c72559f0a0fb77bd715",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 0,
+ "blockHash": "0xb7feda15d99e8d82e7d4acc06119b8bb6274a8e390582c3546ca2750d8a345e1",
+ "l1BatchNumber": 604008
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3221037,
+ "transactionHash": "0x25f9bd9de8260b53e2765cddb6aaafce5256fca647434c72559f0a0fb77bd715",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x397b33b307fc137878ebfc75b295289ec0ee25a31bb5bf034f33256fe8ea2aa6",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 1,
+ "blockHash": "0xb7feda15d99e8d82e7d4acc06119b8bb6274a8e390582c3546ca2750d8a345e1",
+ "l1BatchNumber": 604008
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3221037,
+ "transactionHash": "0x25f9bd9de8260b53e2765cddb6aaafce5256fca647434c72559f0a0fb77bd715",
+ "address": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "topics": [
+ "0xb84fba9af218da60d299dc177abd5805e7ac541d2673cbee7808c10017874f63",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 2,
+ "blockHash": "0xb7feda15d99e8d82e7d4acc06119b8bb6274a8e390582c3546ca2750d8a345e1",
+ "l1BatchNumber": 604008
+ }
+ ],
+ "blockNumber": 3221037,
+ "confirmations": 1086203,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604008,
+ "l1BatchTxIndex": 87,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 3221037,
+ "blockHash": "0xb7feda15d99e8d82e7d4acc06119b8bb6274a8e390582c3546ca2750d8a345e1",
+ "l1BatchNumber": 604008,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x25f9bd9de8260b53e2765cddb6aaafce5256fca647434c72559f0a0fb77bd715",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x25f9bd9de8260b53e2765cddb6aaafce5256fca647434c72559f0a0fb77bd715",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/transfer.json b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/transfer.json
new file mode 100644
index 0000000000..b01ec772a8
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/transfer.json
@@ -0,0 +1,62 @@
+{
+ "to": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xe19ac7f291ceaf4c3f0f6a62481c6ab513e2d7ed8c8b08668ee39d1bfcfb3e3a",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x3650db"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xe19ac7f291ceaf4c3f0f6a62481c6ab513e2d7ed8c8b08668ee39d1bfcfb3e3a",
+ "transactionHash": "0xf8835220234eecd1a6dfd4dc1be8594e6f076d73107497b665a97a6d694320ad",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226875,
+ "transactionHash": "0xf8835220234eecd1a6dfd4dc1be8594e6f076d73107497b665a97a6d694320ad",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000018083f408d000",
+ "logIndex": 0,
+ "blockHash": "0xe19ac7f291ceaf4c3f0f6a62481c6ab513e2d7ed8c8b08668ee39d1bfcfb3e3a",
+ "l1BatchNumber": 604080
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226875,
+ "transactionHash": "0xf8835220234eecd1a6dfd4dc1be8594e6f076d73107497b665a97a6d694320ad",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000c9593dc3dcad5f3804aaa5af12a9d74d0c00e4b0"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 1,
+ "blockHash": "0xe19ac7f291ceaf4c3f0f6a62481c6ab513e2d7ed8c8b08668ee39d1bfcfb3e3a",
+ "l1BatchNumber": 604080
+ }
+ ],
+ "blockNumber": 3226875,
+ "confirmations": 1082155,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604080,
+ "l1BatchTxIndex": 73,
+ "l2ToL1Logs": [],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/withdrawal-to-diff-address.json b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/withdrawal-to-diff-address.json
new file mode 100644
index 0000000000..a1784f4d30
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/withdrawal-to-diff-address.json
@@ -0,0 +1,121 @@
+{
+ "to": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x51e4f1"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226822,
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x00000000000000000000000000000000000000000000000000027673d9c05b00",
+ "logIndex": 0,
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "l1BatchNumber": 604079
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226822,
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 1,
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "l1BatchNumber": 604079
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226822,
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x9b5b9a05e4726d8bb959f1440e05c6b8109443f2083bc4e386237d7654526553",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 2,
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "l1BatchNumber": 604079
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226822,
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "address": "0x0000000000000000000000000000000000008008",
+ "topics": [
+ "0x3a36e47291f4201faf137fab081d92295bce2d53be2c6ca68ba82c7faa9ce241",
+ "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "0x2cfd37fb896e8af6dca014289f581ecbb498fb5662c0d1bc8bbdcee4880294d4"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004c11a2ccc1c9593dc3dcad5f3804aaa5af12a9d74d0c00e4b0d35cceead182dcee0f148ebac9447da2c4d449c400000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000",
+ "logIndex": 3,
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "l1BatchNumber": 604079
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226822,
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "address": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "topics": [
+ "0x2fc3848834aac8e883a2d2a17a7514dc4f2d3dd268089df9b9f5d918259ef3b0",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000c9593dc3dcad5f3804aaa5af12a9d74d0c00e4b0",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 4,
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "l1BatchNumber": 604079
+ }
+ ],
+ "blockNumber": 3226822,
+ "confirmations": 1081736,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604079,
+ "l1BatchTxIndex": 111,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 3226822,
+ "blockHash": "0x7f8dfc45375d15bbe541aef4b018f2dbea46758da3b76e2bc6846833d79b4bc3",
+ "l1BatchNumber": 604079,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008008",
+ "key": "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "value": "0x2cfd37fb896e8af6dca014289f581ecbb498fb5662c0d1bc8bbdcee4880294d4",
+ "transactionHash": "0x85dc1a3b141cdc67ed5f787c688d9ea8976363c875b5c4d3347cac69bcd23108",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/withdrawal.json b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/withdrawal.json
new file mode 100644
index 0000000000..ae80ed2e3d
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/pre-approved-erc20/withdrawal.json
@@ -0,0 +1,121 @@
+{
+ "to": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "from": "0x481E48Ce19781c3cA573967216deE75FDcF70F54",
+ "contractAddress": null,
+ "transactionIndex": 0,
+ "root": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x521e43"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "logs": [
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226739,
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "address": "0x000000000000000000000000000000000000800A",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000008001"
+ ],
+ "data": "0x000000000000000000000000000000000000000000000000000277b0afc08300",
+ "logIndex": 0,
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "l1BatchNumber": 604078
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226739,
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 1,
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "l1BatchNumber": 604078
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226739,
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "address": "0x852a4599217E76aA725F0AdA8BF832a1F57a8A91",
+ "topics": [
+ "0x9b5b9a05e4726d8bb959f1440e05c6b8109443f2083bc4e386237d7654526553",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 2,
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "l1BatchNumber": 604078
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226739,
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "address": "0x0000000000000000000000000000000000008008",
+ "topics": [
+ "0x3a36e47291f4201faf137fab081d92295bce2d53be2c6ca68ba82c7faa9ce241",
+ "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "0x9f76d9d39cd9b74822deed512743bcd0a4f6be0c365c3815eea3ed01ccd0e182"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004c11a2ccc1481e48ce19781c3ca573967216dee75fdcf70f54d35cceead182dcee0f148ebac9447da2c4d449c400000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000",
+ "logIndex": 3,
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "l1BatchNumber": 604078
+ },
+ {
+ "transactionIndex": 0,
+ "blockNumber": 3226739,
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "address": "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
+ "topics": [
+ "0x2fc3848834aac8e883a2d2a17a7514dc4f2d3dd268089df9b9f5d918259ef3b0",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000481e48ce19781c3ca573967216dee75fdcf70f54",
+ "0x000000000000000000000000852a4599217e76aa725f0ada8bf832a1f57a8a91"
+ ],
+ "data": "0x0000000000000000000000000000000000000000000000000000000000989680",
+ "logIndex": 4,
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "l1BatchNumber": 604078
+ }
+ ],
+ "blockNumber": 3226739,
+ "confirmations": 1081404,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x05f5e100"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 604078,
+ "l1BatchTxIndex": 72,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 3226739,
+ "blockHash": "0xdfaf26d4ed764860df979bb730f0c09f4b6a6281647daf4a6abe8173120a1848",
+ "l1BatchNumber": 604078,
+ "transactionIndex": 0,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008008",
+ "key": "0x000000000000000000000000c7e0220d02d549c4846a6ec31d89c3b670ebe35c",
+ "value": "0x9f76d9d39cd9b74822deed512743bcd0a4f6be0c365c3815eea3ed01ccd0e182",
+ "transactionHash": "0x37f670de38b93e28c3ecf5ede9b4c96a4d26f2aa6c53bb6ffc7a040f559d8abb",
+ "logIndex": 0
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/test/transactionReceipts/tx-with-no-logs.json b/packages/data-fetcher/test/transactionReceipts/tx-with-no-logs.json
new file mode 100644
index 0000000000..61bb658f92
--- /dev/null
+++ b/packages/data-fetcher/test/transactionReceipts/tx-with-no-logs.json
@@ -0,0 +1,45 @@
+{
+ "to": "0x0000000000000000000000000000000000000000",
+ "from": "0xc4dE8832cB330ffDF0CC10F9cEFCb5bE36cd5F95",
+ "contractAddress": null,
+ "transactionIndex": 2,
+ "root": "0xdd0d8320c9b12c31c4c6697d6c38fe6431a4fb899bf6eb755fd21fc6a05af34b",
+ "gasUsed": {
+ "type": "BigNumber",
+ "hex": "0x2dc6"
+ },
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "blockHash": "0xdd0d8320c9b12c31c4c6697d6c38fe6431a4fb899bf6eb755fd21fc6a05af34b",
+ "transactionHash": "0x7a91bf03b42c7c3f5bcd7f82bb2855313faaac42d0148937a955a867b4cf2202",
+ "logs": [],
+ "blockNumber": 4318388,
+ "confirmations": 1100,
+ "cumulativeGasUsed": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "effectiveGasPrice": {
+ "type": "BigNumber",
+ "hex": "0x00"
+ },
+ "status": 1,
+ "type": 0,
+ "l1BatchNumber": 615958,
+ "l1BatchTxIndex": 201,
+ "l2ToL1Logs": [
+ {
+ "blockNumber": 4318388,
+ "blockHash": "0xdd0d8320c9b12c31c4c6697d6c38fe6431a4fb899bf6eb755fd21fc6a05af34b",
+ "l1BatchNumber": 615958,
+ "transactionIndex": 2,
+ "shardId": 0,
+ "isService": true,
+ "sender": "0x0000000000000000000000000000000000008001",
+ "key": "0x7a91bf03b42c7c3f5bcd7f82bb2855313faaac42d0148937a955a867b4cf2202",
+ "value": "0x0000000000000000000000000000000000000000000000000000000000000001",
+ "transactionHash": "0x7a91bf03b42c7c3f5bcd7f82bb2855313faaac42d0148937a955a867b4cf2202",
+ "logIndex": 1
+ }
+ ],
+ "byzantium": true
+}
\ No newline at end of file
diff --git a/packages/data-fetcher/tsconfig.build.json b/packages/data-fetcher/tsconfig.build.json
new file mode 100644
index 0000000000..763e062315
--- /dev/null
+++ b/packages/data-fetcher/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "test", "dist", "scripts", "**/*spec.ts"]
+}
diff --git a/packages/data-fetcher/tsconfig.json b/packages/data-fetcher/tsconfig.json
new file mode 100644
index 0000000000..880a66e224
--- /dev/null
+++ b/packages/data-fetcher/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "declaration": true,
+ "removeComments": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
+ "target": "es2017",
+ "sourceMap": true,
+ "outDir": "./dist",
+ "baseUrl": "./",
+ "incremental": true,
+ "skipLibCheck": true,
+ "strictNullChecks": false,
+ "noImplicitAny": false,
+ "strictBindCallApply": false,
+ "forceConsistentCasingInFileNames": false,
+ "noFallthroughCasesInSwitch": false,
+ "resolveJsonModule": true
+ }
+}
\ No newline at end of file
diff --git a/packages/integration-tests/.env.example b/packages/integration-tests/.env.example
index 8cbab2a5aa..c65faa8c16 100644
--- a/packages/integration-tests/.env.example
+++ b/packages/integration-tests/.env.example
@@ -1 +1 @@
-WALLET_PRIVATE_KEY=
\ No newline at end of file
+WALLET_PRIVATE_KEY=
diff --git a/packages/integration-tests/README.md b/packages/integration-tests/README.md
index 14ee629b25..e989cf1693 100644
--- a/packages/integration-tests/README.md
+++ b/packages/integration-tests/README.md
@@ -15,7 +15,7 @@ npm install
cp .env.example packages/integration-tests/src/playbook/.env
```
-- Add your private wallet key to `WALLET_PRIVATE_KEY=` By default we use `richWalletPrivateKey` private key in `integration-tests/src/playbook/utils/getWallet.ts` from `integration-tests/src/entities.ts`.
+- Add your private wallet key to `WALLET_PRIVATE_KEY={YOUR_PRIVATE_KEY}`. By default we use `richWalletPrivateKey` private key in `integration-tests/src/playbook/utils/getWallet.ts` from `integration-tests/src/entities.ts`.
## Preparing a local environment
@@ -42,7 +42,7 @@ npm run integration-test:ui
If you need to run the exact test or/and suite you can change
key-words from `it` to `fit` (for the test) and `describe` to `fdescribe` for suite.
-If you need to exclude some specific test/suites, you can change keywords `it` to `xit` and/or
+If you need to exclude some specific test/suite, you can change keywords `it` to `xit` and/or
`describe` to `xdescribe`.
The test solution contains two main folders: [src](./src) and [tests](./tests).
diff --git a/packages/integration-tests/jest.config.json b/packages/integration-tests/jest.config.json
index 6657349178..4a0226d4bd 100644
--- a/packages/integration-tests/jest.config.json
+++ b/packages/integration-tests/jest.config.json
@@ -10,4 +10,4 @@
},
"reporters": ["default"],
"maxWorkers": 1
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/entities.ts b/packages/integration-tests/src/entities.ts
index 15c1c375a1..4b936901cd 100644
--- a/packages/integration-tests/src/entities.ts
+++ b/packages/integration-tests/src/entities.ts
@@ -30,6 +30,8 @@ export enum Buffer {
emptyWalletAddress = "./buffer/emptyWalletAddress.txt",
failedState = "./buffer/failedState.txt",
customToken = "./buffer/customToken.txt",
+ txEthDeposit = "./buffer/txEthDeposit.txt",
+ txERC20Deposit = "./buffer/txERC20Deposit.txt",
}
export enum Logger {
@@ -58,16 +60,16 @@ export enum TransactionsType {
withdrawal = "withdrawal",
}
-export enum TransactionsStatus {
+export enum TransactionStatus {
failed = "failed",
}
export enum Wallets {
mainWalletAddress = "0x586607935E1462ab762F438E0A7b2968A4158975",
- secondWalletAddress = "0x26A4c5Dfe2cA3c9E7E8C417B689F41b6b5745C37",
+ secondaryWalletAddress = "0x26A4c5Dfe2cA3c9E7E8C417B689F41b6b5745C37",
richWalletAddress = "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049",
mainWalletPrivateKey = "0x06ac1584dd1cf69f97a784b2b7812cd0c65a867ec997add028cdf56483c1c299",
- secondWalletPrivateKey = "e14e6e0b3b610411cf15c3a5aa3252cac9e0a40a9bbe67ceb3b5d506f56576fd",
+ secondaryWalletPrivateKey = "e14e6e0b3b610411cf15c3a5aa3252cac9e0a40a9bbe67ceb3b5d506f56576fd",
richWalletPrivateKey = "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110",
}
diff --git a/packages/integration-tests/src/helper.ts b/packages/integration-tests/src/helper.ts
index 6afe3f590e..e58906afc3 100644
--- a/packages/integration-tests/src/helper.ts
+++ b/packages/integration-tests/src/helper.ts
@@ -79,7 +79,7 @@ export class Helper {
* with a delay between attempts (localConfig.intervalAPIretries).
* Throws an error if the action consistently fails after all retries.
*/
- async retryTestAction(action) {
+ async runRetriableTestAction(action) {
for (let i = 0; i < localConfig.maxAPIretries; i++) {
try {
await action();
diff --git a/packages/integration-tests/src/playbook/contracts/Caller.sol b/packages/integration-tests/src/playbook/contracts/Caller.sol
index 06fbb8d08a..7028ee5148 100644
--- a/packages/integration-tests/src/playbook/contracts/Caller.sol
+++ b/packages/integration-tests/src/playbook/contracts/Caller.sol
@@ -25,4 +25,4 @@ contract GCaller {
function newCallGreeter() external view returns (string memory) {
return IGreeter2(callAddress).callGreeter();
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/contracts/Greeter.sol b/packages/integration-tests/src/playbook/contracts/Greeter.sol
index a387ffea88..77f0cd9c63 100644
--- a/packages/integration-tests/src/playbook/contracts/Greeter.sol
+++ b/packages/integration-tests/src/playbook/contracts/Greeter.sol
@@ -19,4 +19,4 @@ contract Greeter {
emit SetGreeting(_greeting);
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/contracts/L1.sol b/packages/integration-tests/src/playbook/contracts/L1.sol
index 9ab266f345..189c55b609 100644
--- a/packages/integration-tests/src/playbook/contracts/L1.sol
+++ b/packages/integration-tests/src/playbook/contracts/L1.sol
@@ -9,4 +9,4 @@ contract L1 is ERC20 {
constructor(address _recipient) public ERC20("L1 ERC20 token", "L1") {
_mint(_recipient, _initial_supply);
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/contracts/L2.sol b/packages/integration-tests/src/playbook/contracts/L2.sol
index d75566e808..3d56815a0f 100644
--- a/packages/integration-tests/src/playbook/contracts/L2.sol
+++ b/packages/integration-tests/src/playbook/contracts/L2.sol
@@ -9,4 +9,4 @@ contract L2 is ERC20 {
constructor() ERC20("L2 ERC20 token", "L2") {
_mint(msg.sender, _initial_supply);
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/contracts/Middle.sol b/packages/integration-tests/src/playbook/contracts/Middle.sol
index 1064289caa..6a45f3e3f0 100644
--- a/packages/integration-tests/src/playbook/contracts/Middle.sol
+++ b/packages/integration-tests/src/playbook/contracts/Middle.sol
@@ -20,4 +20,4 @@ contract Middle {
function callGreeter() external view returns (string memory) {
return IGreeter(myAddress).greet();
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/contracts/MyERC20.sol b/packages/integration-tests/src/playbook/contracts/MyERC20.sol
index 0814c573e0..5eb5a63d78 100644
--- a/packages/integration-tests/src/playbook/contracts/MyERC20.sol
+++ b/packages/integration-tests/src/playbook/contracts/MyERC20.sol
@@ -23,4 +23,4 @@ contract MyERC20 is ERC20 {
function decimals() public view override returns (uint8) {
return _decimals;
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/contracts/Root.sol b/packages/integration-tests/src/playbook/contracts/Root.sol
index b5f3b2abc7..8c21e2e273 100644
--- a/packages/integration-tests/src/playbook/contracts/Root.sol
+++ b/packages/integration-tests/src/playbook/contracts/Root.sol
@@ -19,4 +19,4 @@ contract Root {
emit SetGreeting(_greeting);
}
-}
\ No newline at end of file
+}
diff --git a/packages/integration-tests/src/playbook/deploy/use-multitransferETH.ts b/packages/integration-tests/src/playbook/deploy/use-multitransferETH.ts
index 5995db64d5..b28765f570 100644
--- a/packages/integration-tests/src/playbook/deploy/use-multitransferETH.ts
+++ b/packages/integration-tests/src/playbook/deploy/use-multitransferETH.ts
@@ -20,7 +20,7 @@ export default async function callMultiTransferETH(hre: HardhatRuntimeEnvironmen
//wallets, To
const richWalletAddress = Wallets.richWalletAddress;
const mainWalletAddress = Wallets.mainWalletAddress;
- const secondWalletAddress = Wallets.secondWalletAddress;
+ const secondaryWalletAddress = Wallets.secondaryWalletAddress;
// type of coin, contract
const etherAddress = Token.ETHER_Address; //ETH
const customTokenI = await helper.getStringFromFile(firstToken);
@@ -60,7 +60,7 @@ export default async function callMultiTransferETH(hre: HardhatRuntimeEnvironmen
//call the deployed contract.
const transferFromContract = await attachedContract.multiTransfer(
- [richWalletAddress, mainWalletAddress, secondWalletAddress],
+ [richWalletAddress, mainWalletAddress, secondaryWalletAddress],
[etherAddress, customTokenI, customTokenII],
[ethAmount, customTokenIAmount, customTokenIIAmount]
);
@@ -90,7 +90,7 @@ export default async function callMultiTransferETH(hre: HardhatRuntimeEnvironmen
);
console.log(
`balance of wallet 3 is: "${ethers.utils.formatUnits(
- await provider.getBalance(secondWalletAddress, "latest", customTokenII),
+ await provider.getBalance(secondaryWalletAddress, "latest", customTokenII),
18
)}" Custom token II`
);
diff --git a/packages/integration-tests/src/playbook/scenarios/deposit/depositERC20.ts b/packages/integration-tests/src/playbook/scenarios/deposit/depositERC20.ts
index f18724a450..0d3a48d1cb 100644
--- a/packages/integration-tests/src/playbook/scenarios/deposit/depositERC20.ts
+++ b/packages/integration-tests/src/playbook/scenarios/deposit/depositERC20.ts
@@ -11,7 +11,8 @@ const syncProvider = new zksync.Provider(localConfig.L2Network);
const ethProvider = ethers.getDefaultProvider(localConfig.L1Network);
const syncWallet = new zksync.Wallet(localConfig.privateKey, syncProvider, ethProvider);
const playbookRoot = "src/playbook";
-const bufferFile = playbookRoot + "/" + Buffer.L2deposited;
+const bufferAddressL2DepositedFile = playbookRoot + "/" + Buffer.L2deposited;
+const bufferTxErc20DepositFile = playbookRoot + "/" + Buffer.txERC20Deposit;
export const depositERC20 = async function (sum = "0.5", tokenAddress: string, units = 18) {
const deposit = await syncWallet.deposit({
@@ -23,15 +24,14 @@ export const depositERC20 = async function (sum = "0.5", tokenAddress: string, u
overrides: localConfig.gasLimit,
});
- const txHash = deposit.hash;
-
await deposit.wait(1);
- await deposit.waitL1Commit(1);
const l2TokenAddress = await syncProvider.l2TokenAddress(tokenAddress);
console.log("L2 token address ", l2TokenAddress);
- await fs.writeFile(bufferFile, l2TokenAddress);
- await helper.txHashLogger(Logger.deposit, txHash, "ERC20 token");
+ const txHash = await deposit.waitFinalize();
+ await helper.txHashLogger(Logger.deposit, txHash.transactionHash, "ERC20 token");
+ await fs.writeFile(bufferAddressL2DepositedFile, l2TokenAddress);
+ await fs.writeFile(bufferTxErc20DepositFile, txHash.transactionHash);
return txHash;
};
diff --git a/packages/integration-tests/src/playbook/scenarios/deposit/depositETH.ts b/packages/integration-tests/src/playbook/scenarios/deposit/depositETH.ts
index e35e6b6259..f15e27b30b 100644
--- a/packages/integration-tests/src/playbook/scenarios/deposit/depositETH.ts
+++ b/packages/integration-tests/src/playbook/scenarios/deposit/depositETH.ts
@@ -1,8 +1,9 @@
import * as ethers from "ethers";
+import { promises as fs } from "fs";
import * as zksync from "zksync-web3";
import { localConfig } from "../../../config";
-import { Logger } from "../../../entities";
+import { Buffer, Logger } from "../../../entities";
import { Helper } from "../../../helper";
export const depositEth = async function (sum = "0.000009") {
@@ -10,17 +11,18 @@ export const depositEth = async function (sum = "0.000009") {
const syncProvider = new zksync.Provider(localConfig.L2Network);
const ethProvider = ethers.getDefaultProvider(localConfig.L1Network);
const syncWallet = new zksync.Wallet(localConfig.privateKey, syncProvider, ethProvider);
+ const playbookRoot = "src/playbook/";
+ const bufferFile = playbookRoot + Buffer.txEthDeposit;
const deposit = await syncWallet.deposit({
token: zksync.utils.ETH_ADDRESS,
amount: ethers.utils.parseEther(sum),
l2GasLimit: localConfig.l2GasLimit,
});
-
- const txHash = deposit.hash;
-
await deposit.wait(1);
- await helper.txHashLogger(Logger.deposit, txHash, "ETH");
+ const txHash = await deposit.waitFinalize();
+ await helper.txHashLogger(Logger.deposit, txHash.transactionHash, "ETH");
+ await fs.writeFile(bufferFile, txHash.transactionHash);
return txHash;
};
diff --git a/packages/integration-tests/src/playbook/scenarios/transfers/transferERC20.ts b/packages/integration-tests/src/playbook/scenarios/transfers/transferERC20.ts
index 13a1ab0ed3..80f19d3905 100644
--- a/packages/integration-tests/src/playbook/scenarios/transfers/transferERC20.ts
+++ b/packages/integration-tests/src/playbook/scenarios/transfers/transferERC20.ts
@@ -11,7 +11,7 @@ export const transferERC20 = async function (sum: string, tokenAddress: string,
const syncProvider = new zksync.Provider(localConfig.L2Network);
const ethProvider = ethers.getDefaultProvider(localConfig.L1Network);
const syncWallet = new zksync.Wallet(localConfig.privateKey, syncProvider, ethProvider);
- const syncWallet2 = new zksync.Wallet(Wallets.secondWalletPrivateKey, syncProvider, ethProvider);
+ const syncWallet2 = new zksync.Wallet(Wallets.secondaryWalletPrivateKey, syncProvider, ethProvider);
const playbookRoot = "src/playbook/";
const bufferFile = playbookRoot + Buffer.txEthTransfer;
diff --git a/packages/integration-tests/tests/api/tokens.test.ts b/packages/integration-tests/tests/api/tokens.test.ts
index 415d82a1e4..f4b2501648 100644
--- a/packages/integration-tests/tests/api/tokens.test.ts
+++ b/packages/integration-tests/tests/api/tokens.test.ts
@@ -87,7 +87,7 @@ describe("Tokens", () => {
expect(response.status).toBe(200);
expect(response.body.items[0].amount).toBe("10000000000000000");
expect(response.body.items[0].from).toBe(Wallets.richWalletAddress);
- expect(response.body.items[0].to).toBe(Wallets.secondWalletAddress);
+ expect(response.body.items[0].to).toBe(Wallets.secondaryWalletAddress);
expect(response.body.items[0].token).toEqual(expect.objectContaining({ l2Address: l2Token }));
expect(response.body.items[0]).toEqual(expect.objectContaining({ transactionHash: txHash }));
expect(response.body.items[0]).toEqual(expect.objectContaining({ type: "transfer" }));
diff --git a/packages/integration-tests/tests/api/transactions.test.ts b/packages/integration-tests/tests/api/transactions.test.ts
index cdf0d08f96..7510bceddd 100644
--- a/packages/integration-tests/tests/api/transactions.test.ts
+++ b/packages/integration-tests/tests/api/transactions.test.ts
@@ -42,6 +42,33 @@ describe("Transactions", () => {
response = await helper.performGETrequest(apiRoute);
expect(response.status).toBe(200);
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ to: Token.ETHER_PULL_Address }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({ tokenAddress: Token.ETHER_ERC20_Address })
+ );
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: "fee" }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({
+ token: {
+ decimals: 18,
+ iconURL: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266",
+ l1Address: Token.ETHER_Address,
+ l2Address: Token.ETHER_ERC20_Address,
+ liquidity: 220000000000,
+ symbol: "ETH",
+ name: "Ether",
+ usdPrice: 1800,
+ },
+ })
+ );
expect(response.body.items[1].from).toBe(Wallets.richWalletAddress);
expect(response.body.items[1].to).toBe(Wallets.mainWalletAddress);
expect(response.body.items[1].transactionHash).toBe(txHash);
@@ -62,6 +89,29 @@ describe("Transactions", () => {
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ to: Token.ETHER_PULL_Address }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: "fee" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({ tokenAddress: Token.ETHER_ERC20_Address })
+ );
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({
+ token: {
+ decimals: 18,
+ iconURL: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266",
+ l1Address: Token.ETHER_Address,
+ l2Address: Token.ETHER_ERC20_Address,
+ liquidity: 220000000000,
+ symbol: "ETH",
+ name: "Ether",
+ usdPrice: 1800,
+ },
+ })
+ );
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ to: Token.ETHER_ERC20_Address }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
@@ -91,6 +141,29 @@ describe("Transactions", () => {
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ to: Token.ETHER_PULL_Address }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: "fee" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({ tokenAddress: Token.ETHER_ERC20_Address })
+ );
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({
+ token: {
+ decimals: 18,
+ iconURL: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266",
+ l1Address: Token.ETHER_Address,
+ l2Address: Token.ETHER_ERC20_Address,
+ liquidity: 220000000000,
+ symbol: "ETH",
+ name: "Ether",
+ usdPrice: 1800,
+ },
+ })
+ );
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ to: Token.ETHER_ERC20_Address }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
@@ -263,6 +336,37 @@ describe("Transactions", () => {
expect(response.body.to).toBe("0x000000000000000000000000000000000000800A");
expect(response.body.from).toBe(Wallets.richWalletAddress);
expect(response.body.value).toBe("9000000000000");
+ expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -276,6 +380,39 @@ describe("Transactions", () => {
expect(response.status).toBe(200);
expect(response.body.hash).toBe(txHash);
expect(response.body.from).toBe(Wallets.richWalletAddress);
+ expect(typeof response.body.to).toStrictEqual("string");
+ expect(response.body.value).toBe("0");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -291,6 +428,37 @@ describe("Transactions", () => {
expect(response.body.to).toBe("0x000000000000000000000000000000000000800A");
expect(response.body.from).toBe(Wallets.richWalletAddress);
expect(response.body.value).toBe("9000000000000");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -307,6 +475,37 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ to: contract }));
expect(response.body).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(response.body.value).toBe("101000000000000000");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -320,9 +519,40 @@ describe("Transactions", () => {
expect(response.status).toBe(200);
expect(response.body).toStrictEqual(expect.objectContaining({ hash: txHash }));
- // expect(response.body).toStrictEqual(expect.objectContaining({ to: contract })) //unstable on CI
+ expect(response.body).toStrictEqual(expect.objectContaining({ to: contract }));
expect(response.body).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(response.body.value).toBe("0");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -339,6 +569,37 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ to: contract }));
expect(response.body).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(response.body.value).toBe("0");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -355,6 +616,38 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ to: contract }));
expect(response.body).toStrictEqual(expect.objectContaining({ value: "0" }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
+ expect(typeof response.body.from).toStrictEqual("string");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -371,6 +664,38 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ value: "0" }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(response.body).toStrictEqual(
+ expect.objectContaining({ to: "0x0000000000000000000000000000000000008006" })
+ );
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -387,6 +712,38 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ value: "0" }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(response.body).toStrictEqual(
+ expect.objectContaining({ to: "0x0000000000000000000000000000000000008006" })
+ );
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -403,6 +760,38 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ value: "0" }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(response.body).toStrictEqual(
+ expect.objectContaining({ to: "0x0000000000000000000000000000000000008006" })
+ );
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -419,6 +808,36 @@ describe("Transactions", () => {
expect(response.body).toStrictEqual(expect.objectContaining({ value: "0" }));
expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.to).toStrictEqual("string");
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ revertReason: null }));
+ expect(typeof response.body.status).toStrictEqual("string");
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
@@ -435,6 +854,39 @@ describe("Transactions", () => {
expect(response.body.to).toStrictEqual(token);
expect(response.body.hash).toStrictEqual(txHash);
expect(response.body.status).toStrictEqual(TransactionsStatus.failed);
+ expect(response.body).toStrictEqual(expect.objectContaining({ value: "0" }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ isL1Originated: false }));
+ expect(response.body).toStrictEqual(expect.objectContaining({ transactionIndex: 0 }));
+ expect(typeof response.body.data).toStrictEqual("string");
+ expect(typeof response.body.fee).toStrictEqual("string");
+ expect(typeof response.body.nonce).toStrictEqual("number");
+ expect(typeof response.body.gasLimit).toStrictEqual("string");
+ expect(typeof response.body.gasPrice).toStrictEqual("string");
+ expect(typeof response.body.gasPerPubdata).toStrictEqual("string");
+ expect(typeof response.body.maxFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.maxPriorityFeePerGas).toStrictEqual("string");
+ expect(typeof response.body.blockNumber).toStrictEqual("number");
+ expect(response.body.blockNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.l1BatchNumber).toStrictEqual("number");
+ expect(response.body.l1BatchNumber).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.blockHash).toStrictEqual("string");
+ expect(response.body.blockHash.length).toBe(66);
+ expect(typeof response.body.type).toStrictEqual("number");
+ expect(response.body.type).toBeGreaterThanOrEqual(0);
+ expect(typeof response.body.receivedAt).toStrictEqual("string");
+ expect(response.body.receivedAt.length).toBe(24);
+ expect(response.body).toStrictEqual(expect.objectContaining({ error: null }));
+ expect(response.body).toStrictEqual(
+ expect.objectContaining({ revertReason: "ERC20: transfer to the zero address" })
+ );
+ expect(typeof response.body.commitTxHash).toStrictEqual("string");
+ expect(response.body.commitTxHash.length).toBe(66);
+ expect(typeof response.body.executeTxHash).toStrictEqual("string");
+ expect(response.body.executeTxHash.length).toBe(66);
+ expect(typeof response.body.proveTxHash).toStrictEqual("string");
+ expect(response.body.proveTxHash.length).toBe(66);
+ expect(typeof response.body.isL1BatchSealed).toStrictEqual("boolean");
+ expect(typeof response.body.gasUsed).toStrictEqual("string");
});
});
});
@@ -461,7 +913,12 @@ describe("Transactions", () => {
expect.objectContaining({ tokenAddress: Token.ETHER_ERC20_Address })
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
@@ -541,6 +998,25 @@ describe("Transactions", () => {
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({
+ token: {
+ decimals: 18,
+ iconURL: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266",
+ l1Address: Token.ETHER_Address,
+ l2Address: Token.ETHER_ERC20_Address,
+ liquidity: 220000000000,
+ symbol: "ETH",
+ name: "Ether",
+ usdPrice: 1800,
+ },
+ })
+ );
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ to: contract }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
@@ -605,6 +1081,25 @@ describe("Transactions", () => {
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
+ expect(response.body.items[0]).toStrictEqual(
+ expect.objectContaining({
+ token: {
+ decimals: 18,
+ iconURL: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266",
+ l1Address: Token.ETHER_Address,
+ l2Address: Token.ETHER_ERC20_Address,
+ liquidity: 220000000000,
+ symbol: "ETH",
+ name: "Ether",
+ usdPrice: 1800,
+ },
+ })
+ );
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ from: Wallets.richWalletAddress }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ to: contract }));
expect(response.body.items[1]).toStrictEqual(expect.objectContaining({ transactionHash: txHash }));
@@ -668,6 +1163,11 @@ describe("Transactions", () => {
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenAddress: token }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.transfer }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ERC20" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
@@ -750,6 +1250,7 @@ describe("Transactions", () => {
expect.objectContaining({ tokenAddress: Token.ETHER_ERC20_Address })
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
@@ -825,6 +1326,11 @@ describe("Transactions", () => {
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
@@ -881,6 +1387,11 @@ describe("Transactions", () => {
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
@@ -937,6 +1448,11 @@ describe("Transactions", () => {
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
@@ -992,6 +1508,11 @@ describe("Transactions", () => {
);
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ type: TransactionsType.fee }));
expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ fields: null }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ isInternal: false }));
+ expect(response.body.items[0]).toStrictEqual(expect.objectContaining({ tokenType: "ETH" }));
+ expect(typeof response.body.items[0].blockNumber).toStrictEqual("number");
+ expect(typeof response.body.items[0].timestamp).toStrictEqual("string");
+ expect(typeof response.body.items[0].amount).toStrictEqual("string");
expect(response.body.items[0]).toStrictEqual(
expect.objectContaining({
token: {
diff --git a/packages/integration-tests/tests/ui/deposit.spec.ts b/packages/integration-tests/tests/ui/deposit.spec.ts
new file mode 100644
index 0000000000..5d6f5f2431
--- /dev/null
+++ b/packages/integration-tests/tests/ui/deposit.spec.ts
@@ -0,0 +1,53 @@
+import { expect, test } from "@playwright/test";
+
+import { config } from "./config";
+import { BlockExplorer, Buffer, Wallets } from "../../src/entities";
+import { Helper } from "../../src/helper";
+
+import type { Locator } from "@playwright/test";
+
+const bufferRoute = "src/playbook/";
+const helper = new Helper();
+let url: string;
+let bufferFile;
+let depositTxHash: string;
+let initiatorAddress: string;
+let hash, initiatorAddressElement, ethValue, erc20Value: Locator;
+
+//@id1660
+test("Check Deposit ETH transaction on BE", async ({ page }) => {
+ bufferFile = bufferRoute + Buffer.txEthDeposit;
+ depositTxHash = await helper.getStringFromFile(bufferFile);
+ url = BlockExplorer.baseUrl + `/tx/${depositTxHash}` + BlockExplorer.localNetwork;
+ initiatorAddress = Wallets.richWalletAddress;
+
+ await page.goto(url);
+
+ hash = await page.locator(`//*[text()="Transaction Hash"]/..//..//*[text()="${depositTxHash}"]`).first();
+ initiatorAddressElement = await page.locator(`text=${initiatorAddress}`).first();
+ ethValue = await page.locator(`text=0.0000001`).first();
+
+ await expect(hash).toBeVisible(config.defaultTimeout);
+ await expect(ethValue).toBeVisible(config.defaultTimeout);
+ await expect(initiatorAddressElement).toBeVisible(config.defaultTimeout);
+});
+
+//@id1681
+test("Check on BE Deposit the custom ERC-20 token", async ({ page }) => {
+ bufferFile = bufferRoute + Buffer.txERC20Deposit;
+ depositTxHash = await helper.getStringFromFile(bufferFile);
+ url = BlockExplorer.baseUrl + `/tx/${depositTxHash}` + BlockExplorer.localNetwork;
+ initiatorAddress = Wallets.richWalletAddress;
+
+ await page.goto(url);
+
+ hash = await page.locator(`//*[text()="Transaction Hash"]/..//..//*[text()="${depositTxHash}"]`).first();
+ initiatorAddressElement = await page.locator(
+ `//*[text()="From"]/..//*[text()="L1"]/..//*[text()="0x36615Cf349d...c049"]`
+ );
+ erc20Value = await page.locator(`//*[text()="0x36615Cf349d...c049"]/..//..//*[text()="100"]`);
+
+ await expect(hash).toBeVisible(config.defaultTimeout);
+ await expect(erc20Value).toBeVisible(config.defaultTimeout);
+ await expect(initiatorAddressElement).toBeVisible(config.defaultTimeout);
+});
diff --git a/packages/integration-tests/tsconfig.json b/packages/integration-tests/tsconfig.json
index 12cebf4b35..adb614cab7 100644
--- a/packages/integration-tests/tsconfig.json
+++ b/packages/integration-tests/tsconfig.json
@@ -18,4 +18,4 @@
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
-}
\ No newline at end of file
+}
diff --git a/packages/worker/.env.example b/packages/worker/.env.example
index 7402762a37..d06ef87b32 100644
--- a/packages/worker/.env.example
+++ b/packages/worker/.env.example
@@ -9,9 +9,12 @@ DATABASE_CONNECTION_IDLE_TIMEOUT_MS=12000
DATABASE_CONNECTION_POOL_SIZE=100
BLOCKCHAIN_RPC_URL=http://localhost:3050
+DATA_FETCHER_URL=http://localhost:3040
+DATA_FETCHER_REQUEST_TIMEOUT=120000
WAIT_FOR_BLOCKS_INTERVAL=1000
BLOCKS_PROCESSING_BATCH_SIZE=10
+NUMBER_OF_BLOCKS_PER_DB_TRANSACTION=10
BATCHES_PROCESSING_POLLING_INTERVAL=60000
DELETE_BALANCES_INTERVAL=300000
@@ -26,7 +29,6 @@ COLLECT_BLOCKS_TO_PROCESS_METRIC_INTERVAL=10000
DISABLE_BATCHES_PROCESSING=false
DISABLE_COUNTERS_PROCESSING=false
-DISABLE_BALANCES_PROCESSING=false
DISABLE_OLD_BALANCES_CLEANER=false
DISABLE_BLOCKS_REVERT=false
diff --git a/packages/worker/README.md b/packages/worker/README.md
index bd75aaf656..8040408ea1 100644
--- a/packages/worker/README.md
+++ b/packages/worker/README.md
@@ -1,7 +1,7 @@
# zkSync Era Block Explorer Worker
## Overview
-`zkSync Era Block Explorer Worker` is an indexer service for zkSync Era blockchain data. The purpose of the service is to read the data from the blockchain in real time, transform it and fill in it's database with the data in a way that makes it easy to read by the [Block explorer API](/packages/api).
+`zkSync Era Block Explorer Worker` is an indexer service for zkSync Era blockchain data. It retrieves aggregated data from the [Data Fetcher](/packages/data-fetcher) via HTTP and also directly from the blockchain using [zkSync Era JSON-RPC API](https://era.zksync.io/docs/api/api.html), processes it and saves into the database in a way that makes it easy to read by the [Block Explorer API](/packages/api).
## Installation
@@ -16,6 +16,7 @@ $ npm install
cp .env.example .env
```
- In order to tell the service where to get the blockchain data from set the value of the `BLOCKCHAIN_RPC_URL` env var to your blockchain RPC API URL. For zkSync Era testnet it can be set to `https://zksync2-testnet.zksync.dev`. For zkSync Era mainnet - `https://zksync2-mainnet.zksync.io`.
+- To retrieve aggregated blockchain data for a certain block, the Worker service calls the [Data Fetcher](/packages/data-fetcher) service via HTTP. To specify Data Fetcher URL use `DATA_FETCHER_URL` env variable. By default, it is set to `http://localhost:3040` which is a default value for the local environment.
- Set up env variables for Postgres database connection. By default it points to `localhost:5432` and database name is `block-explorer`.
You need to have a running Postgres server, set the following env variables to point the service to your database:
- `DATABASE_HOST`
diff --git a/packages/worker/package.json b/packages/worker/package.json
index 53f9decf39..f56ada9107 100644
--- a/packages/worker/package.json
+++ b/packages/worker/package.json
@@ -92,10 +92,10 @@
"coverageDirectory": "../coverage",
"coverageThreshold": {
"global": {
- "branches": 100,
- "functions": 100,
- "lines": 100,
- "statements": 100
+ "branches": 95,
+ "functions": 90,
+ "lines": 95,
+ "statements": 95
}
},
"testEnvironment": "node",
diff --git a/packages/worker/src/app.module.ts b/packages/worker/src/app.module.ts
index 7fc4504e81..bec09a377a 100644
--- a/packages/worker/src/app.module.ts
+++ b/packages/worker/src/app.module.ts
@@ -12,10 +12,7 @@ import { BlocksRevertService } from "./blocksRevert";
import { BatchService } from "./batch";
import { BlockProcessor, BlockWatcher, BlockService } from "./block";
import { TransactionProcessor } from "./transaction";
-import { LogProcessor } from "./log";
-import { AddressService } from "./address/address.service";
import { BalanceService, BalancesCleanerService } from "./balance";
-import { TransferService } from "./transfer/transfer.service";
import { TokenService } from "./token/token.service";
import { TokenOffChainDataProvider } from "./token/tokenOffChainData/tokenOffChainDataProvider.abstract";
import { CoingeckoTokenOffChainDataProvider } from "./token/tokenOffChainData/providers/coingecko/coingeckoTokenOffChainDataProvider";
@@ -54,6 +51,7 @@ import { RetryDelayProvider } from "./retryDelay.provider";
import { MetricsModule } from "./metrics";
import { DbMetricsService } from "./dbMetrics.service";
import { UnitOfWorkModule } from "./unitOfWork";
+import { DataFetcherService } from "./dataFetcher/dataFetcher.service";
@Module({
imports: [
@@ -94,10 +92,9 @@ import { UnitOfWorkModule } from "./unitOfWork";
providers: [
AppService,
BlockchainService,
- AddressService,
+ DataFetcherService,
BalanceService,
BalancesCleanerService,
- TransferService,
TokenService,
{
provide: TokenOffChainDataProvider,
@@ -128,7 +125,6 @@ import { UnitOfWorkModule } from "./unitOfWork";
BatchService,
BlockProcessor,
TransactionProcessor,
- LogProcessor,
BlockWatcher,
BlockService,
Logger,
diff --git a/packages/worker/src/balance/balance.service.spec.ts b/packages/worker/src/balance/balance.service.spec.ts
index 5e7a9293f5..5a64f4dd33 100644
--- a/packages/worker/src/balance/balance.service.spec.ts
+++ b/packages/worker/src/balance/balance.service.spec.ts
@@ -1,20 +1,14 @@
import { Test, TestingModuleBuilder } from "@nestjs/testing";
import { Logger } from "@nestjs/common";
import { mock } from "jest-mock-extended";
-import { BigNumber } from "ethers";
-import { utils } from "zksync-web3";
-import { ConfigService } from "@nestjs/config";
-import { Transfer } from "../transfer/interfaces/transfer.interface";
-import { BlockchainService } from "../blockchain/blockchain.service";
import { BalanceRepository } from "../repositories";
import { TokenType } from "../entities";
+import { Balance as ChangedBalance } from "../dataFetcher/types";
import { BalanceService } from "./";
describe("BalanceService", () => {
let testingModuleBuilder: TestingModuleBuilder;
- let blockchainServiceMock: BlockchainService;
let balanceRepositoryMock: BalanceRepository;
- let configServiceMock: ConfigService;
let balanceService: BalanceService;
let startDeleteOldBalancesDurationMetricMock: jest.Mock;
@@ -23,11 +17,6 @@ describe("BalanceService", () => {
let stopDeleteZeroBalancesDurationMetricMock: jest.Mock;
beforeEach(async () => {
- configServiceMock = mock({
- get: jest.fn().mockReturnValue(false),
- });
-
- blockchainServiceMock = mock();
balanceRepositoryMock = mock({
deleteOldBalances: jest.fn().mockResolvedValue(null),
});
@@ -40,10 +29,6 @@ describe("BalanceService", () => {
testingModuleBuilder = Test.createTestingModule({
providers: [
BalanceService,
- {
- provide: BlockchainService,
- useValue: blockchainServiceMock,
- },
{
provide: BalanceRepository,
useValue: balanceRepositoryMock,
@@ -60,10 +45,6 @@ describe("BalanceService", () => {
startTimer: startDeleteZeroBalancesDurationMetricMock,
},
},
- {
- provide: ConfigService,
- useValue: configServiceMock,
- },
],
});
const app = await testingModuleBuilder.compile();
@@ -73,582 +54,55 @@ describe("BalanceService", () => {
balanceService = app.get(BalanceService);
});
- describe("clearTrackedState", () => {
- const blockNumber = 10;
- const blockNumber2 = 15;
-
- beforeEach(() => {
- balanceService.changedBalances.set(
- blockNumber,
- new Map>()
- );
- balanceService.changedBalances.set(
- blockNumber2,
- new Map>()
- );
- });
-
- it("clears tracked balances for the specified block number", () => {
- balanceService.clearTrackedState(blockNumber);
- expect(balanceService.changedBalances.size).toBe(1);
- expect(balanceService.changedBalances.has(blockNumber2)).toBe(true);
- });
- });
-
- describe("trackChangedBalances", () => {
- const transfers = [
- mock({
- tokenAddress: "0x000000000000000000000000000000000000800a",
- from: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
- to: "0x0000000000000000000000000000000000008001",
- blockNumber: 10,
- tokenType: TokenType.ETH,
- }),
- mock({
- tokenAddress: "0x000000000000000000000000000000000000800a",
- from: "0xd206eaf6819007535e893410cfa01885ce40e99a",
- to: "0x0000000000000000000000000000000000008001",
- blockNumber: 10,
- tokenType: TokenType.ETH,
- }),
- mock({
- tokenAddress: "0x2392e98fb47cf05773144db3ce8002fac4f39c84",
- from: "0x0000000000000000000000000000000000000000",
- to: "0x36615cf349d7f6344891b1e7ca7c72883f5dc049",
- blockNumber: 10,
- tokenType: TokenType.ERC20,
- }),
+ describe("saveChangedBalances", () => {
+ const changesBalances = [
+ { address: "address1", tokenAddress: "tokenAddresses1", balance: "100" } as ChangedBalance,
+ { address: "address2", tokenAddress: "tokenAddresses2", balance: "200" } as ChangedBalance,
];
- it("processes null as a transfers array", () => {
- balanceService.trackChangedBalances(null);
- expect(balanceService.changedBalances.size).toBe(0);
- });
-
- it("processes empty transfers array", () => {
- balanceService.trackChangedBalances([]);
- expect(balanceService.changedBalances.size).toBe(0);
- });
-
- it("does not track changed balance for 0x000 address", () => {
- const transfers = [
- mock