diff --git a/.dialyzer-ignore b/.dialyzer-ignore index c36e55ab7ae5..6ea02019b20d 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -1,28 +1,12 @@ -:0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3 -:0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2 -:0: Unknown type 'Elixir.Map':t/0 -:0: Unknown type 'Elixir.Hash':t/0 -:0: Unknown type 'Elixir.Address':t/0 lib/ethereum_jsonrpc/rolling_window.ex:171 lib/explorer/smart_contract/solidity/publisher_worker.ex:1 lib/explorer/smart_contract/vyper/publisher_worker.ex:1 lib/explorer/smart_contract/solidity/publisher_worker.ex:8 lib/explorer/smart_contract/vyper/publisher_worker.ex:8 -lib/block_scout_web/router.ex:1 -lib/block_scout_web/schema/types.ex:31 -lib/phoenix/router.ex:324 lib/phoenix/router.ex:402 lib/explorer/smart_contract/reader.ex:435 lib/explorer/exchange_rates/source.ex:139 lib/explorer/exchange_rates/source.ex:142 -lib/indexer/fetcher/polygon_edge.ex:737 -lib/indexer/fetcher/polygon_edge/deposit_execute.ex:140 -lib/indexer/fetcher/polygon_edge/deposit_execute.ex:184 -lib/indexer/fetcher/polygon_edge/withdrawal.ex:160 -lib/indexer/fetcher/polygon_edge/withdrawal.ex:204 -lib/indexer/fetcher/zkevm/transaction_batch.ex:116 -lib/indexer/fetcher/zkevm/transaction_batch.ex:156 -lib/indexer/fetcher/zkevm/transaction_batch.ex:252 lib/block_scout_web/views/api/v2/transaction_view.ex:431 lib/block_scout_web/views/api/v2/transaction_view.ex:472 -lib/explorer/chain/transaction.ex:166 +lib/explorer/chain/transaction.ex:171 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d8441328e0cc..5e94950bad16 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -24,8 +24,8 @@ body: description: How the application has been deployed. options: - Docker-compose + - Helm charts (k8s) - Manual from the source code - - Helm charts - Docker validations: required: true @@ -65,7 +65,7 @@ body: attributes: label: Backend version description: The release version of the backend or branch/commit. - placeholder: v5.3.1 + placeholder: v6.1.0 validations: required: true diff --git a/.github/actions/setup-repo-and-short-sha/action.yml b/.github/actions/setup-repo-and-short-sha/action.yml new file mode 100644 index 000000000000..d3cce9891a3a --- /dev/null +++ b/.github/actions/setup-repo-and-short-sha/action.yml @@ -0,0 +1,23 @@ +name: 'Setup repo and calc short SHA commit' +description: 'Setup repo: checkout/login/extract metadata, Set up Docker Buildx and calculate short SHA commit' +inputs: + docker-username: + description: 'Docker username' + required: true + docker-password: + description: 'Docker password' + required: true +runs: + using: "composite" + + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ inputs.docker-username }} + docker-password: ${{ inputs.docker-password }} + + - name: Add SHORT_SHA env property with commit short sha + shell: bash + run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/actions/setup-repo/action.yml b/.github/actions/setup-repo/action.yml new file mode 100644 index 000000000000..2c3533159e15 --- /dev/null +++ b/.github/actions/setup-repo/action.yml @@ -0,0 +1,29 @@ +name: 'Setup repo' +description: 'Setup repo: checkout/login/extract metadata, Set up Docker Buildx' +inputs: + docker-username: + description: 'Docker username' + required: true + docker-password: + description: 'Docker password' + required: true +runs: + using: "composite" + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ inputs.docker-username }} + password: ${{ inputs.docker-password }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: blockscout/blockscout \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b5eaa004db36..7ff4b3ed8af5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: directory: "/apps/block_scout_web/assets" open-pull-requests-limit: 10 schedule: - interval: "weekly" + interval: "monthly" ignore: - dependency-name: "bootstrap" - dependency-name: "web3" @@ -20,4 +20,4 @@ updates: directory: "/apps/explorer" open-pull-requests-limit: 10 schedule: - interval: "weekly" + interval: "monthly" diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 497c5005104d..0f2ad51ee036 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -4,34 +4,52 @@ on: push: branches: - master - - production-core-stg - - production-eth-stg-experimental - - production-eth-goerli-stg - - production-eth-sepolia-stg - - production-fuse-stg - - production-optimism-stg - - production-immutable-stg - - production-iota-stg - - production-lukso-stg - - production-rsk-stg - - production-sokol-stg - - production-suave-stg - - production-xdai-stg - - production-zkevm-stg - - production-zksync-stg + - production-core + - production-eth-experimental + - production-eth-goerli + - production-eth-sepolia + - production-fuse + - production-optimism + - production-immutable + - production-iota + - production-lukso + - production-rsk + - production-sokol + - production-suave + - production-xdai + - production-zkevm + - production-zksync - staging-l2 + paths-ignore: + - 'CHANGELOG.md' + - '**/README.md' + - 'docker/*' + - 'docker-compose/*' pull_request: branches: - master - - production-optimism-stg + - production-optimism + - production-zksync env: MIX_ENV: test - OTP_VERSION: '25.2.1' - ELIXIR_VERSION: '1.14.5' - ACCOUNT_AUTH0_DOMAIN: 'blockscoutcom.us.auth0.com' + OTP_VERSION: ${{ vars.OTP_VERSION || '25.3.2.8' }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION || '1.14.5' }} + ACCOUNT_AUTH0_DOMAIN: "blockscoutcom.us.auth0.com" jobs: + matrix-builder: + name: Build matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: | + echo "matrix=$matrixStringifiedObject" >> $GITHUB_OUTPUT + env: + matrixStringifiedObject: '{"chain-type": ["ethereum", "polygon_edge", "polygon_zkevm", "rsk", "suave", "stability", "filecoin", "optimism"]}' + build-and-cache: name: Build and Cache deps runs-on: ubuntu-latest @@ -41,6 +59,9 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: "ELIXIR_VERSION.lock" run: echo "${ELIXIR_VERSION}" > ELIXIR_VERSION.lock @@ -49,15 +70,15 @@ jobs: run: echo "${OTP_VERSION}" > OTP_VERSION.lock - name: Restore Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps- + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Conditionally build Mix deps cache if: steps.deps-cache.outputs.cache-hit != 'true' @@ -65,10 +86,10 @@ jobs: mix local.hex --force mix local.rebar --force mix deps.get - mix deps.compile + mix deps.compile --skip-umbrella-children - name: Restore Explorer NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: explorer-npm-cache with: path: apps/explorer/node_modules @@ -82,7 +103,7 @@ jobs: working-directory: apps/explorer - name: Restore Blockscout Web NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: blockscoutweb-npm-cache with: path: apps/block_scout_web/assets/node_modules @@ -105,17 +126,20 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Restore Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - run: mix credo @@ -129,58 +153,74 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Restore Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - run: mix format --check-formatted + dialyzer: + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix-builder.outputs.matrix) }} name: Dialyzer static analysis runs-on: ubuntu-latest - needs: build-and-cache + needs: + - build-and-cache + - matrix-builder steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Restore Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Restore Dialyzer Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: dialyzer-cache with: path: priv/plts - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-dialyzer-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-${{ matrix.chain-type }}-dialyzer-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-dialyzer-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-${{ matrix.chain-type }}-dialyzer-mixlockhash- - name: Conditionally build Dialyzer Cache if: steps.dialyzer-cache.output.cache-hit != 'true' run: | mkdir -p priv/plts mix dialyzer --plt + env: + CHAIN_TYPE: ${{ matrix.chain-type }} - name: Run Dialyzer run: mix dialyzer --halt-exit-status + env: + CHAIN_TYPE: ${{ matrix.chain-type }} gettext: name: Missing translation keys check @@ -192,17 +232,20 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Restore Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - run: | mix gettext.extract --merge | tee stdout.txt @@ -218,17 +261,20 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Scan explorer for vulnerabilities run: mix sobelow --config @@ -247,20 +293,23 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Restore Explorer NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: explorer-npm-cache with: path: apps/explorer/node_modules @@ -269,7 +318,7 @@ jobs: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-explorer-npm- - name: Restore Blockscout Web NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: blockscoutweb-npm-cache with: path: apps/block_scout_web/assets/node_modules @@ -295,20 +344,23 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Restore Explorer NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: explorer-npm-cache with: path: apps/explorer/node_modules @@ -317,7 +369,7 @@ jobs: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-explorer-npm- - name: Restore Blockscout Web NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: blockscoutweb-npm-cache with: path: apps/block_scout_web/assets/node_modules @@ -341,20 +393,23 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Restore Blockscout Web NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: blockscoutweb-npm-cache with: path: apps/block_scout_web/assets/node_modules @@ -370,12 +425,17 @@ jobs: working-directory: apps/block_scout_web/assets test_nethermind_mox_ethereum_jsonrpc: + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix-builder.outputs.matrix) }} name: EthereumJSONRPC Tests runs-on: ubuntu-latest - needs: build-and-cache + needs: + - build-and-cache + - matrix-builder services: postgres: - image: postgres + image: postgres:15 env: # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database POSTGRES_DB: explorer_test @@ -398,17 +458,20 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - run: ./bin/install_chrome_headless.sh - name: mix test --exclude no_nethermind @@ -423,13 +486,19 @@ jobs: PGUSER: postgres ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.Mox" ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" + CHAIN_TYPE: "${{ matrix.chain-type }}" test_nethermind_mox_explorer: + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix-builder.outputs.matrix) }} name: Explorer Tests runs-on: ubuntu-latest - needs: build-and-cache + needs: + - build-and-cache + - matrix-builder services: postgres: - image: postgres + image: postgres:15 env: # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database POSTGRES_DB: explorer_test @@ -452,20 +521,23 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Restore Explorer NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: explorer-npm-cache with: path: apps/explorer/node_modules @@ -488,13 +560,19 @@ jobs: PGUSER: postgres ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.Mox" ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" + CHAIN_TYPE: "${{ matrix.chain-type }}" test_nethermind_mox_indexer: + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix-builder.outputs.matrix) }} name: Indexer Tests runs-on: ubuntu-latest - needs: build-and-cache + needs: + - build-and-cache + - matrix-builder services: postgres: - image: postgres + image: postgres:15 env: # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database POSTGRES_DB: explorer_test @@ -517,18 +595,20 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" - + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - run: ./bin/install_chrome_headless.sh @@ -546,19 +626,24 @@ jobs: PGUSER: postgres ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.Mox" ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" - + CHAIN_TYPE: "${{ matrix.chain-type }}" test_nethermind_mox_block_scout_web: + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix-builder.outputs.matrix) }} name: Blockscout Web Tests runs-on: ubuntu-latest - needs: build-and-cache + needs: + - build-and-cache + - matrix-builder services: - redis_db: - image: 'redis:alpine' - ports: + redis-db: + image: "redis:alpine" + ports: - 6379:6379 postgres: - image: postgres + image: postgres:15 env: # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database POSTGRES_DB: explorer_test @@ -581,21 +666,23 @@ jobs: with: otp-version: ${{ env.OTP_VERSION }} elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex - name: Mix Deps Cache - uses: actions/cache@v2 + uses: actions/cache/restore@v4 id: deps-cache with: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_24-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} restore-keys: | - ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" - + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- - name: Restore Explorer NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: explorer-npm-cache with: path: apps/explorer/node_modules @@ -604,7 +691,7 @@ jobs: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-explorer-npm- - name: Restore Blockscout Web NPM Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: blockscoutweb-npm-cache with: path: apps/block_scout_web/assets/node_modules @@ -637,5 +724,5 @@ jobs: ADMIN_PANEL_ENABLED: "true" ACCOUNT_ENABLED: "true" ACCOUNT_REDIS_URL: "redis://localhost:6379" - API_V2_ENABLED: "true" SOURCIFY_INTEGRATION_ENABLED: "true" + CHAIN_TYPE: "${{ matrix.chain-type }}" diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 000000000000..7eba93b933d0 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,98 @@ +name: Pre-release master + +on: + workflow_dispatch: + inputs: + number: + type: number + required: true + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image for Ethereum + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-ethereum:latest, blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + platforms: | + linux/amd64 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum + + - name: Build & Push Docker image for Shibarium + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=blockscout/blockscout-shibarium:buildcache + cache-to: type=registry,ref=blockscout/blockscout-shibarium:buildcache,mode=max + tags: blockscout/blockscout-shibarium:latest, blockscout/blockscout-shibarium:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + platforms: | + linux/amd64 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=shibarium + + - name: Build & Push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=blockscout/blockscout:buildcache + cache-to: type=registry,ref=blockscout/blockscout:buildcache,mode=max + tags: blockscout/blockscout:master, blockscout/blockscout:latest, blockscout/blockscout:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 25d5b90e1470..6ec06ee0e85d 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -4,10 +4,14 @@ on: push: branches: - master + paths-ignore: + - 'CHANGELOG.md' + - '**/README.md' + - 'docker-compose/*' env: - OTP_VERSION: '25.2.1' - ELIXIR_VERSION: '1.14.5' - RELEASE_VERSION: 5.3.1 + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} jobs: push_to_registry: @@ -17,26 +21,13 @@ jobs: release-version: ${{ steps.output-step.outputs.release-version }} short-sha: ${{ steps.output-step.outputs.short-sha }} steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - images: blockscout/blockscout + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV - name: Add outputs run: | diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 25bb973ee358..9f726bfbbd56 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: POA Core Publish Docker image on: + workflow_dispatch: push: branches: - - production-core-stg + - production-core jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: poa steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-goerli.yml b/.github/workflows/publish-docker-image-for-eth-goerli.yml index f871e4fa0b3b..262802e27ec2 100644 --- a/.github/workflows/publish-docker-image-for-eth-goerli.yml +++ b/.github/workflows/publish-docker-image-for-eth-goerli.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: ETH Goerli Publish Docker image on: + workflow_dispatch: push: branches: - - production-eth-goerli-stg + - production-eth-goerli jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: eth-goerli steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -43,6 +28,7 @@ jobs: push: true tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} build-args: | + CHAIN_TYPE=ethereum CACHE_EXCHANGE_RATES_PERIOD= API_V1_READ_METHODS_DISABLED=false DISABLE_WEBAPP=false diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml new file mode 100644 index 000000000000..d63d935df4a6 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -0,0 +1,40 @@ +name: ETH Sepolia Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-eth-sepolia +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + DOCKER_CHAIN_NAME: eth-sepolia + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + build-args: | + CHAIN_TYPE=ethereum + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 5ea173e78ae7..3e3bed50190d 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: ETH Publish Docker image on: + workflow_dispatch: push: branches: - - production-eth-stg-experimental + - production-eth-experimental jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: mainnet steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -43,6 +28,7 @@ jobs: push: true tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-experimental build-args: | + CHAIN_TYPE=ethereum CACHE_EXCHANGE_RATES_PERIOD= API_V1_READ_METHODS_DISABLED=false DISABLE_WEBAPP=false diff --git a/.github/workflows/publish-docker-image-for-rsk.yml b/.github/workflows/publish-docker-image-for-filecoin.yml similarity index 53% rename from .github/workflows/publish-docker-image-for-rsk.yml rename to .github/workflows/publish-docker-image-for-filecoin.yml index 424aece44e8b..ca720971fbbc 100644 --- a/.github/workflows/publish-docker-image-for-rsk.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -1,39 +1,23 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - name: Publish Docker image for specific chain branches on: push: branches: - - production-rsk-stg + - production-filecoin jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 - DOCKER_CHAIN_NAME: rsk + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + DOCKER_CHAIN_NAME: filecoin steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -52,4 +36,4 @@ jobs: CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=rsk \ No newline at end of file + CHAIN_TYPE=filecoin \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 1f7501eab860..bb88fc294b50 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -1,42 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Fuse Publish Docker image on: + workflow_dispatch: push: branches: - - production-fuse-stg + - production-fuse jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: fuse steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -46,6 +28,7 @@ jobs: push: true tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} build-args: | + BRIDGED_TOKENS_ENABLED=true CACHE_EXCHANGE_RATES_PERIOD= API_V1_READ_METHODS_DISABLED=false DISABLE_WEBAPP=false diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml new file mode 100644 index 000000000000..86f59dfd0643 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -0,0 +1,41 @@ +name: Gnosis Chain Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-xdai +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + DOCKER_CHAIN_NAME: xdai + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + build-args: | + BRIDGED_TOKENS_ENABLED=true + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-immutable.yml b/.github/workflows/publish-docker-image-for-immutable.yml index 67b0a7b70974..21ea26f23395 100644 --- a/.github/workflows/publish-docker-image-for-immutable.yml +++ b/.github/workflows/publish-docker-image-for-immutable.yml @@ -1,42 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Immutable Publish Docker image on: + workflow_dispatch: push: branches: - - production-immutable-stg + - production-immutable jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: immutable steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 21d2641d1d00..4ced3d5a35d1 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -1,11 +1,7 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: L2 staging Publish Docker image on: + workflow_dispatch: push: branches: - staging-l2 @@ -14,26 +10,15 @@ jobs: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index bc601f4bd280..35e01599c831 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: LUKSO Publish Docker image on: + workflow_dispatch: push: branches: - - production-lukso-stg + - production-lukso jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: lukso steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index e352d4c7353e..13361808767f 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -1,42 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Optimism Publish Docker image on: + workflow_dispatch: push: branches: - - production-optimism-stg + - production-optimism jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: optimism steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -57,4 +39,5 @@ jobs: ADMIN_PANEL_ENABLED=false CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} \ No newline at end of file + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-polygon-edge.yml b/.github/workflows/publish-docker-image-for-polygon-edge.yml index a4de66bcfcd1..e5bcbf6b2a34 100644 --- a/.github/workflows/publish-docker-image-for-polygon-edge.yml +++ b/.github/workflows/publish-docker-image-for-polygon-edge.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Polygon Edge Publish Docker image on: + workflow_dispatch: push: branches: - - production-polygon-edge-stg + - production-polygon-edge jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: polygon-edge steps: - - name: Check out the repo - uses: actions/checkout@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml new file mode 100644 index 000000000000..4a4c90e6f178 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -0,0 +1,40 @@ +name: Rootstock Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-rsk +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + DOCKER_CHAIN_NAME: rsk + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=rsk \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-shibarium.yml b/.github/workflows/publish-docker-image-for-shibarium.yml new file mode 100644 index 000000000000..8496b598eec3 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-shibarium.yml @@ -0,0 +1,43 @@ +name: Shibarium Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-shibarium +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + DOCKER_CHAIN_NAME: shibarium + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=shibarium \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-stability.yml b/.github/workflows/publish-docker-image-for-stability.yml index 57fd1e0fcb7c..b5f486595e0e 100644 --- a/.github/workflows/publish-docker-image-for-stability.yml +++ b/.github/workflows/publish-docker-image-for-stability.yml @@ -1,45 +1,27 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Stability Publish Docker image on: + workflow_dispatch: push: branches: - - production-stability-stg + - production-stability env: - OTP_VERSION: '25.2.1' - ELIXIR_VERSION: '1.14.5' + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: stability steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 3e66ed92dc2b..d7d28a9e0fa2 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -1,45 +1,27 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: SUAVE Publish Docker image on: + workflow_dispatch: push: branches: - - production-suave-stg + - production-suave env: - OTP_VERSION: '25.2.1' - ELIXIR_VERSION: '1.14.5' + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: suave steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-xdai.yml b/.github/workflows/publish-docker-image-for-xdai.yml deleted file mode 100644 index 6706ec927eb3..000000000000 --- a/.github/workflows/publish-docker-image-for-xdai.yml +++ /dev/null @@ -1,60 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches - -on: - push: - branches: - - production-xdai-stg -jobs: - push_to_registry: - name: Push Docker image to Docker Hub - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 5.3.1 - DOCKER_CHAIN_NAME: xdai - steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} - build-args: | - CACHE_EXCHANGE_RATES_PERIOD= - API_V1_READ_METHODS_DISABLED=false - DISABLE_WEBAPP=false - API_V1_WRITE_METHODS_DISABLED=false - CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= - ADMIN_PANEL_ENABLED=false - CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= - DISABLE_BRIDGE_MARKET_CAP_UPDATER=false - CACHE_BRIDGE_MARKET_CAP_UPDATE_INTERVAL= - SENTRY_DSN_CLIENT_GNOSIS=${{ secrets.SENTRY_DSN_CLIENT_GNOSIS }} - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml new file mode 100644 index 000000000000..0abd04fe2cca --- /dev/null +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -0,0 +1,40 @@ +name: Zetachain publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-zetachain +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + DOCKER_CHAIN_NAME: zetachain + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zetachain \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 374cae48a909..74ab92177a9f 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Zkevm publish Docker image on: + workflow_dispatch: push: branches: - - production-zkevm-stg + - production-zkevm jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: zkevm steps: - - name: Check out the repo - uses: actions/checkout@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index aee2f7f70a90..1b746bf26983 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -1,39 +1,24 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for specific chain branches +name: Zksync publish Docker image on: + workflow_dispatch: push: branches: - - production-zksync-stg + - production-zksync jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} DOCKER_CHAIN_NAME: zksync steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Add SHORT_SHA env property with commit short sha - run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 @@ -51,4 +36,5 @@ jobs: ADMIN_PANEL_ENABLED=false CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} \ No newline at end of file + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zksync \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml new file mode 100644 index 000000000000..bf3f48c890bc --- /dev/null +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -0,0 +1,61 @@ +name: Publish Docker image to staging on demand + +on: + workflow_dispatch: + push: + branches: + - staging + paths-ignore: + - 'CHANGELOG.md' + - '**/README.md' + - 'docker-compose/*' +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + outputs: + release-version: ${{ steps.output-step.outputs.release-version }} + short-sha: ${{ steps.output-step.outputs.short-sha }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo-and-short-sha + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Add outputs + run: | + echo "::set-output name=release-version::${{ env.NEXT_RELEASE_VERSION }}" + echo "::set-output name=short-sha::${{ env.SHORT_SHA }}" + id: output-step + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=blockscout/blockscout:buildcache + cache-to: type=registry,ref=blockscout/blockscout:buildcache,mode=max + tags: blockscout/blockscout-staging:latest, blockscout/blockscout-staging:${{ env.RELEASE_VERSION }}.commit.${{ env.SHORT_SHA }} + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/publish-docker-image-release-additional.yml b/.github/workflows/release-additional.yml similarity index 55% rename from .github/workflows/publish-docker-image-release-additional.yml rename to .github/workflows/release-additional.yml index 9877068bff23..a16c4511080a 100644 --- a/.github/workflows/publish-docker-image-release-additional.yml +++ b/.github/workflows/release-additional.yml @@ -1,42 +1,26 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image for some custom chains +name: Release additional on: release: types: [published] env: - OTP_VERSION: '25.2.1' - ELIXIR_VERSION: '1.14.5' + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo with: - images: blockscout/blockscout + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push Docker image for Rootstock uses: docker/build-push-action@v5 @@ -82,7 +66,7 @@ jobs: RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=polygon_edge - - name: Build and push Docker image + - name: Build and push Docker image for Stability uses: docker/build-push-action@v5 with: context: . @@ -102,4 +86,46 @@ jobs: CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=stability \ No newline at end of file + CHAIN_TYPE=stability + - name: Build and push Docker image for Shibarium + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-shibarium:latest, blockscout/blockscout-shibarium:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=shibarium + - name: Build and push Docker image for Fuse + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-fuse:latest, blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BRIDGED_TOKENS_ENABLED=true + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} \ No newline at end of file diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml new file mode 100644 index 000000000000..b6d1b6743e49 --- /dev/null +++ b/.github/workflows/release-eth.yml @@ -0,0 +1,45 @@ +name: Release for Ethereum + +on: + release: + types: [published] + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image for Ethereum + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-ethereum:latest, blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum \ No newline at end of file diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml new file mode 100644 index 000000000000..d8b77901a75d --- /dev/null +++ b/.github/workflows/release-filecoin.yml @@ -0,0 +1,45 @@ +name: Release for Filecoin + +on: + release: + types: [published] + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image for Filecoin + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-filecoin:latest, blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=filecoin \ No newline at end of file diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml new file mode 100644 index 000000000000..bdabb752b213 --- /dev/null +++ b/.github/workflows/release-gnosis.yml @@ -0,0 +1,46 @@ +name: Release for Gnosis Chain + +on: + release: + types: [published] + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image for Gnosis chain + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-xdai:latest, blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BRIDGED_TOKENS_ENABLED=true + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum \ No newline at end of file diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml new file mode 100644 index 000000000000..ed99f437262f --- /dev/null +++ b/.github/workflows/release-optimism.yml @@ -0,0 +1,45 @@ +name: Release for Ethereum + +on: + release: + types: [published] + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image for Optimism + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-optimism:latest, blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism \ No newline at end of file diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml new file mode 100644 index 000000000000..cd9c5abc14c9 --- /dev/null +++ b/.github/workflows/release-zetachain.yml @@ -0,0 +1,45 @@ +name: Release for Zetachain + +on: + release: + types: [published] + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + with: + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image for Zetachain + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: blockscout/blockscout-zetachain:latest, blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + CACHE_EXCHANGE_RATES_PERIOD= + API_V1_READ_METHODS_DISABLED=false + DISABLE_WEBAPP=false + API_V1_WRITE_METHODS_DISABLED=false + CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED= + ADMIN_PANEL_ENABLED=false + CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zetachain \ No newline at end of file diff --git a/.github/workflows/publish-docker-image-release.yml b/.github/workflows/release.yml similarity index 80% rename from .github/workflows/publish-docker-image-release.yml rename to .github/workflows/release.yml index df9aa7d44a9f..a5b98c420728 100644 --- a/.github/workflows/publish-docker-image-release.yml +++ b/.github/workflows/release.yml @@ -1,44 +1,28 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Publish Docker image +name: Release on: release: types: [published] env: - OTP_VERSION: '25.2.1' - ELIXIR_VERSION: '1.14.5' + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest env: - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: ${{ vars.RELEASE_VERSION }} steps: - - name: Check out the repo - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + docker-username: ${{ secrets.DOCKER_USERNAME }} + docker-password: ${{ secrets.DOCKER_PASSWORD }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: blockscout/blockscout - - - name: Build & Push Docker image + - name: Build & Push Core Docker image uses: docker/build-push-action@v5 with: context: . @@ -109,7 +93,6 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-beta RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=suave - - name: Send release announcement to Slack workflow id: slack uses: slackapi/slack-github-action@v1.24.0 @@ -127,15 +110,15 @@ jobs: # runs-on: ubuntu-latest # env: # BRANCHES: | - # production-core-stg - # production-sokol-stg - # production-eth-stg-experimental - # production-eth-goerli-stg - # production-lukso-stg - # production-xdai-stg - # production-polygon-supernets-stg - # production-rsk-stg - # production-immutable-stg + # production-core + # production-sokol + # production-eth-experimental + # production-eth-goerli + # production-lukso + # production-xdai + # production-polygon-supernets + # production-rsk + # production-immutable # steps: # - uses: actions/checkout@v4 # - name: Set Git config diff --git a/.gitignore b/.gitignore index 9e53084c62bb..bfd06c0a3aac 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,8 @@ dump.rdb .vscode **.dec** + +*.env +*.env.example +*.env.local +*.env.staging diff --git a/.tool-versions b/.tool-versions index 13dabe9920d1..32fecf31ec4f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.14.5-otp-25 -erlang 25.3.2.6 +erlang 25.3.2.8 nodejs 18.17.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5c3f8edf61..e2073100db10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,503 @@ ### Fixes +### Chore + +
+ Dependencies version bumps + +
+ +## 6.3.0 + +### Features + +- [#9631](https://github.com/blockscout/blockscout/pull/9631) - Initial support of zksync chain type +- [#9532](https://github.com/blockscout/blockscout/pull/9532) - Add last output root size counter +- [#9511](https://github.com/blockscout/blockscout/pull/9511) - Separate errors by type in EndpointAvailabilityObserver +- [#9490](https://github.com/blockscout/blockscout/pull/9490), [#9644](https://github.com/blockscout/blockscout/pull/9644) - Add blob transaction counter and filter in block view +- [#9486](https://github.com/blockscout/blockscout/pull/9486) - Massive blocks fetcher +- [#9483](https://github.com/blockscout/blockscout/pull/9483) - Add secondary coin and transaction stats +- [#9473](https://github.com/blockscout/blockscout/pull/9473) - Add user_op interpretation +- [#9461](https://github.com/blockscout/blockscout/pull/9461) - Fetch blocks without internal transactions backwards +- [#9460](https://github.com/blockscout/blockscout/pull/9460) - Optimism chain type +- [#9409](https://github.com/blockscout/blockscout/pull/9409) - ETH JSON RPC extension +- [#9390](https://github.com/blockscout/blockscout/pull/9390) - Add stability validators +- [#8702](https://github.com/blockscout/blockscout/pull/8702) - Add OP withdrawal status to transaction page in API +- [#7200](https://github.com/blockscout/blockscout/pull/7200) - Add Optimism BedRock Deposits to the main page in API +- [#6980](https://github.com/blockscout/blockscout/pull/6980) - Add Optimism BedRock support (Txn Batches, Output Roots, Deposits, Withdrawals) + +### Fixes + +- [#9646](https://github.com/blockscout/blockscout/pull/9646) - Hotfix for Optimism Ecotone batch blobs indexing +- [#9640](https://github.com/blockscout/blockscout/pull/9640) - Fix no function clause matching in `BENS.item_to_address_hash_strings/1` +- [#9638](https://github.com/blockscout/blockscout/pull/9638) - Do not broadcast coin balance changes with empty value/delta +- [#9635](https://github.com/blockscout/blockscout/pull/9635) - Reset missing ranges collector to max number after the cycle is done +- [#9629](https://github.com/blockscout/blockscout/pull/9629) - Don't insert pbo for not inserted blocks +- [#9620](https://github.com/blockscout/blockscout/pull/9620) - Fix infinite retries for orphaned blobs +- [#9601](https://github.com/blockscout/blockscout/pull/9601) - Fix token instance transform for some unconventional tokens +- [#9597](https://github.com/blockscout/blockscout/pull/9597) - Update token transfers block_consensus by block_number +- [#9596](https://github.com/blockscout/blockscout/pull/9596) - Fix logging +- [#9585](https://github.com/blockscout/blockscout/pull/9585) - Fix Geth block internal transactions fetching +- [#9576](https://github.com/blockscout/blockscout/pull/9576) - Rewrite query for token transfers on address to eliminate "or" +- [#9572](https://github.com/blockscout/blockscout/pull/9572) - Fix Shibarium L1 fetcher +- [#9563](https://github.com/blockscout/blockscout/pull/9563) - Fix timestamp handler for unfinalized zkEVM batches +- [#9560](https://github.com/blockscout/blockscout/pull/9560) - Fix fetch pending transaction for hyperledger besu client +- [#9555](https://github.com/blockscout/blockscout/pull/9555) - Fix EIP-1967 beacon proxy pattern detection +- [#9529](https://github.com/blockscout/blockscout/pull/9529) - Fix `MAX_SAFE_INTEGER` frontend bug +- [#9518](https://github.com/blockscout/blockscout/pull/9518), [#9628](https://github.com/blockscout/blockscout/pull/9628) - Fix MultipleResultsError in `smart_contract_creation_tx_bytecode/1` +- [#9514](https://github.com/blockscout/blockscout/pull/9514) - Fix missing `0x` prefix for `blockNumber`, `logIndex`, `transactionIndex` and remove `transactionLogIndex` in `eth_getLogs` response. +- [#9510](https://github.com/blockscout/blockscout/pull/9510) - Fix WS false 0 token balances +- [#9512](https://github.com/blockscout/blockscout/pull/9512) - Docker-compose 2.24.6 compatibility +- [#9262](https://github.com/blockscout/blockscout/pull/9262) - Fix withdrawal status +- [#9123](https://github.com/blockscout/blockscout/pull/9123) - Fixes in Optimism due to changed log topics type +- [#8831](https://github.com/blockscout/blockscout/pull/8831) - Return all OP Withdrawals bound to L2 transaction +- [#8822](https://github.com/blockscout/blockscout/pull/8822) - Hotfix for optimism_withdrawal_transaction_status function +- [#8811](https://github.com/blockscout/blockscout/pull/8811) - Consider consensus block only when retrieving OP withdrawal transaction status +- [#8364](https://github.com/blockscout/blockscout/pull/8364) - Fix API v2 for OP Withdrawals +- [#8229](https://github.com/blockscout/blockscout/pull/8229) - Fix Indexer.Fetcher.OptimismTxnBatch +- [#8208](https://github.com/blockscout/blockscout/pull/8208) - Ignore invalid frame by OP transaction batches module +- [#8122](https://github.com/blockscout/blockscout/pull/8122) - Ignore previously handled frame by OP transaction batches module +- [#7827](https://github.com/blockscout/blockscout/pull/7827) - Fix transaction batches module for L2 OP stack +- [#7776](https://github.com/blockscout/blockscout/pull/7776) - Fix transactions ordering in Indexer.Fetcher.OptimismTxnBatch +- [#7219](https://github.com/blockscout/blockscout/pull/7219) - Output L1 fields in API v2 for transaction page and fix transaction fee calculation +- [#6699](https://github.com/blockscout/blockscout/pull/6699) - L1 tx fields fix for Goerli Optimism BedRock update + +### Chore + +- [#9622](https://github.com/blockscout/blockscout/pull/9622) - Add alternative `hex.pm` mirrors +- [#9571](https://github.com/blockscout/blockscout/pull/9571) - Support Optimism Ecotone upgrade by Indexer.Fetcher.Optimism.TxnBatch module +- [#9562](https://github.com/blockscout/blockscout/pull/9562) - Add cancun evm version +- [#9506](https://github.com/blockscout/blockscout/pull/9506) - API v1 bridgedtokenlist endpoint +- [#9260](https://github.com/blockscout/blockscout/pull/9260) - Optimism Delta upgrade support by Indexer.Fetcher.OptimismTxnBatch module +- [#8740](https://github.com/blockscout/blockscout/pull/8740) - Add delay to Indexer.Fetcher.OptimismTxnBatch module initialization + +
+ Dependencies version bumps + +- [#9544](https://github.com/blockscout/blockscout/pull/9544) - Bump @babel/core from 7.23.9 to 7.24.0 in /apps/block_scout_web/assets +- [#9537](https://github.com/blockscout/blockscout/pull/9537) - Bump logger_json from 5.1.3 to 5.1.4 +- [#9550](https://github.com/blockscout/blockscout/pull/9550) - Bump xss from 1.0.14 to 1.0.15 in /apps/block_scout_web/assets +- [#9539](https://github.com/blockscout/blockscout/pull/9539) - Bump floki from 0.35.4 to 0.36.0 +- [#9551](https://github.com/blockscout/blockscout/pull/9551) - Bump @amplitude/analytics-browser from 2.5.1 to 2.5.2 in /apps/block_scout_web/assets +- [#9547](https://github.com/blockscout/blockscout/pull/9547) - Bump @babel/preset-env from 7.23.9 to 7.24.0 in /apps/block_scout_web/assets +- [#9549](https://github.com/blockscout/blockscout/pull/9549) - Bump postcss-loader from 8.1.0 to 8.1.1 in /apps/block_scout_web/assets +- [#9542](https://github.com/blockscout/blockscout/pull/9542) - Bump phoenix_ecto from 4.4.3 to 4.5.0 +- [#9546](https://github.com/blockscout/blockscout/pull/9546) - https://github.com/blockscout/blockscout/pull/9546 +- [#9545](https://github.com/blockscout/blockscout/pull/9545) - Bump chart.js from 4.4.1 to 4.4.2 in /apps/block_scout_web/assets +- [#9540](https://github.com/blockscout/blockscout/pull/9540) - Bump postgrex from 0.17.4 to 0.17.5 +- [#9543](https://github.com/blockscout/blockscout/pull/9543) - Bump ueberauth from 0.10.7 to 0.10.8 +- [#9538](https://github.com/blockscout/blockscout/pull/9538) - Bump credo from 1.7.4 to 1.7.5 +- [#9607](https://github.com/blockscout/blockscout/pull/9607) - Bump redix from 1.3.0 to 1.4.1 +- [#9606](https://github.com/blockscout/blockscout/pull/9606) - Bump ecto from 3.11.1 to 3.11.2 +- [#9605](https://github.com/blockscout/blockscout/pull/9605) - Bump ex_doc from 0.31.1 to 0.31.2 +- [#9604](https://github.com/blockscout/blockscout/pull/9604) - Bump phoenix_ecto from 4.5.0 to 4.5.1 + +
+ +## 6.2.2 + +### Features + +### Fixes + +- [#9505](https://github.com/blockscout/blockscout/pull/9505) - Add env vars for NFT sanitize migration + +### Chore + +- [#9487](https://github.com/blockscout/blockscout/pull/9487) - Add tsvector index on smart_contracts.name + +
+ Dependencies version bumps + +
+ +## 6.2.1 + +### Features + +### Fixes + +- [#9591](https://github.com/blockscout/blockscout/pull/9591) - Fix duplicated results in `methods-read` endpoint +- [#9502](https://github.com/blockscout/blockscout/pull/9502) - Add batch_size and concurrency envs for tt token type migration +- [#9493](https://github.com/blockscout/blockscout/pull/9493) - Fix API response for unknown blob hashes +- [#9484](https://github.com/blockscout/blockscout/pull/9484) - Fix read contract error +- [#9426](https://github.com/blockscout/blockscout/pull/9426) - Fix tabs counter cache bug + +### Chore + +
+ Dependencies version bumps + +- [#9478](https://github.com/blockscout/blockscout/pull/9478) - Bump floki from 0.35.3 to 0.35.4 +- [#9477](https://github.com/blockscout/blockscout/pull/9477) - Bump hammer from 6.2.0 to 6.2.1 +- [#9476](https://github.com/blockscout/blockscout/pull/9476) - Bump eslint from 8.56.0 to 8.57.0 in /apps/block_scout_web/assets +- [#9475](https://github.com/blockscout/blockscout/pull/9475) - Bump @amplitude/analytics-browser from 2.4.1 to 2.5.1 in /apps/block_scout_web/assets +- [#9474](https://github.com/blockscout/blockscout/pull/9474) - Bump sass from 1.71.0 to 1.71.1 in /apps/block_scout_web/assets +- [#9492](https://github.com/blockscout/blockscout/pull/9492) - Bump es5-ext from 0.10.62 to 0.10.64 in /apps/block_scout_web/assets + +
+ +## 6.2.0 + +### Features + +- [#9441](https://github.com/blockscout/blockscout/pull/9441) - Update BENS integration: change endpoint for resolving address in search +- [#9437](https://github.com/blockscout/blockscout/pull/9437) - Add Enum.uniq before sanitizing token transfers +- [#9407](https://github.com/blockscout/blockscout/pull/9407) - ERC-404 basic support +- [#9403](https://github.com/blockscout/blockscout/pull/9403) - Null round handling +- [#9401](https://github.com/blockscout/blockscout/pull/9401) - Eliminate incorrect token transfers with empty token_ids +- [#9396](https://github.com/blockscout/blockscout/pull/9396) - More-Minimal Proxy support +- [#9386](https://github.com/blockscout/blockscout/pull/9386) - Filecoin JSON RPC variant +- [#9379](https://github.com/blockscout/blockscout/pull/9379) - Filter non-traceable transactions for zetachain +- [#9364](https://github.com/blockscout/blockscout/pull/9364) - Fix using of startblock/endblock in API v1 list endpoints: txlist, txlistinternal, tokentx +- [#9360](https://github.com/blockscout/blockscout/pull/9360) - Move missing ranges sanitize to a separate background migration +- [#9351](https://github.com/blockscout/blockscout/pull/9351) - Noves.fi: add proxy endpoint for describeTxs endpoint +- [#9282](https://github.com/blockscout/blockscout/pull/9282) - Add `license_type` to smart contracts +- [#9202](https://github.com/blockscout/blockscout/pull/9202) - Add base and priority fee to gas oracle response +- [#9182](https://github.com/blockscout/blockscout/pull/9182) - Fetch coin balances in async mode in realtime fetcher +- [#9168](https://github.com/blockscout/blockscout/pull/9168) - Support EIP4844 blobs indexing & API +- [#9098](https://github.com/blockscout/blockscout/pull/9098) - Polygon zkEVM Bridge indexer and API v2 extension + +### Fixes + +- [#9444](https://github.com/blockscout/blockscout/pull/9444) - Fix quick search bug +- [#9440](https://github.com/blockscout/blockscout/pull/9440) - Add `debug_traceBlockByNumber` to `method_to_url` +- [#9387](https://github.com/blockscout/blockscout/pull/9387) - Filter out Vyper contracts in Solidityscan API endpoint +- [#9377](https://github.com/blockscout/blockscout/pull/9377) - Speed up account abstraction proxy +- [#9371](https://github.com/blockscout/blockscout/pull/9371) - Filter empty values before token update +- [#9356](https://github.com/blockscout/blockscout/pull/9356) - Remove ERC-1155 logs params from coin balances params +- [#9346](https://github.com/blockscout/blockscout/pull/9346) - Process integer balance in genesis.json +- [#9317](https://github.com/blockscout/blockscout/pull/9317) - Include null gas price txs in fee calculations +- [#9315](https://github.com/blockscout/blockscout/pull/9315) - Fix manual uncle reward calculation +- [#9306](https://github.com/blockscout/blockscout/pull/9306) - Improve marking of failed internal transactions +- [#9305](https://github.com/blockscout/blockscout/pull/9305) - Add effective gas price calculation as fallback +- [#9300](https://github.com/blockscout/blockscout/pull/9300) - Fix read contract bug +- [#9226](https://github.com/blockscout/blockscout/pull/9226) - Split Indexer.Fetcher.TokenInstance.LegacySanitize + +### Chore + +- [#9439](https://github.com/blockscout/blockscout/pull/9439) - Solidityscan integration enhancements +- [#9398](https://github.com/blockscout/blockscout/pull/9398) - Improve elixir dependencies caching in CI +- [#9393](https://github.com/blockscout/blockscout/pull/9393) - Bump actions/cache to v4 +- [#9389](https://github.com/blockscout/blockscout/pull/9389) - Output user address as an object in API v2 for Shibarium +- [#9361](https://github.com/blockscout/blockscout/pull/9361) - Define BRIDGED_TOKENS_ENABLED env in Dockerfile +- [#9257](https://github.com/blockscout/blockscout/pull/9257) - Retry token instance metadata fetch from baseURI + tokenID +- [#8851](https://github.com/blockscout/blockscout/pull/8851) - Fix dialyzer and add TypedEctoSchema + +
+ Dependencies version bumps + +- [#9335](https://github.com/blockscout/blockscout/pull/9335) - Bump mini-css-extract-plugin from 2.7.7 to 2.8.0 in /apps/block_scout_web/assets +- [#9333](https://github.com/blockscout/blockscout/pull/9333) - Bump sweetalert2 from 11.10.3 to 11.10.5 in /apps/block_scout_web/assets +- [#9288](https://github.com/blockscout/blockscout/pull/9288) - Bump solc from 0.8.23 to 0.8.24 in /apps/explorer +- [#9287](https://github.com/blockscout/blockscout/pull/9287) - Bump @babel/preset-env from 7.23.8 to 7.23.9 in /apps/block_scout_web/assets +- [#9331](https://github.com/blockscout/blockscout/pull/9331) - Bump logger_json from 5.1.2 to 5.1.3 +- [#9330](https://github.com/blockscout/blockscout/pull/9330) - Bump hammer from 6.1.0 to 6.2.0 +- [#9294](https://github.com/blockscout/blockscout/pull/9294) - Bump exvcr from 0.15.0 to 0.15.1 +- [#9293](https://github.com/blockscout/blockscout/pull/9293) - Bump floki from 0.35.2 to 0.35.3 +- [#9338](https://github.com/blockscout/blockscout/pull/9338) - Bump postcss-loader from 8.0.0 to 8.1.0 in /apps/block_scout_web/assets +- [#9336](https://github.com/blockscout/blockscout/pull/9336) - Bump web3 from 1.10.3 to 1.10.4 in /apps/block_scout_web/assets +- [#9290](https://github.com/blockscout/blockscout/pull/9290) - Bump ex_doc from 0.31.0 to 0.31.1 +- [#9285](https://github.com/blockscout/blockscout/pull/9285) - Bump @amplitude/analytics-browser from 2.3.8 to 2.4.0 in /apps/block_scout_web/assets +- [#9283](https://github.com/blockscout/blockscout/pull/9283) - Bump @babel/core from 7.23.7 to 7.23.9 in /apps/block_scout_web/assets +- [#9337](https://github.com/blockscout/blockscout/pull/9337) - Bump css-loader from 6.9.1 to 6.10.0 in /apps/block_scout_web/assets +- [#9334](https://github.com/blockscout/blockscout/pull/9334) - Bump sass-loader from 14.0.0 to 14.1.0 in /apps/block_scout_web/assets +- [#9339](https://github.com/blockscout/blockscout/pull/9339) - Bump webpack from 5.89.0 to 5.90.1 in /apps/block_scout_web/assets +- [#9383](https://github.com/blockscout/blockscout/pull/9383) - Bump credo from 1.7.3 to 1.7.4 +- [#9384](https://github.com/blockscout/blockscout/pull/9384) - Bump postcss from 8.4.33 to 8.4.35 in /apps/block_scout_web/assets +- [#9385](https://github.com/blockscout/blockscout/pull/9385) - Bump mixpanel-browser from 2.48.1 to 2.49.0 in /apps/block_scout_web/assets +- [#9423](https://github.com/blockscout/blockscout/pull/9423) - Bump @amplitude/analytics-browser from 2.4.0 to 2.4.1 in /apps/block_scout_web/assets +- [#9422](https://github.com/blockscout/blockscout/pull/9422) - Bump core-js from 3.35.1 to 3.36.0 in /apps/block_scout_web/assets +- [#9424](https://github.com/blockscout/blockscout/pull/9424) - Bump webpack from 5.90.1 to 5.90.3 in /apps/block_scout_web/assets +- [#9425](https://github.com/blockscout/blockscout/pull/9425) - Bump sass-loader from 14.1.0 to 14.1.1 in /apps/block_scout_web/assets +- [#9421](https://github.com/blockscout/blockscout/pull/9421) - Bump sass from 1.70.0 to 1.71.0 in /apps/block_scout_web/assets + +
+ +## 6.1.0 + +### Features + +- [#9189](https://github.com/blockscout/blockscout/pull/9189) - User operations in the search +- [#9169](https://github.com/blockscout/blockscout/pull/9169) - Add bridged tokens functionality to master branch +- [#9158](https://github.com/blockscout/blockscout/pull/9158) - Increase shared memory for PostgreSQL containers +- [#9155](https://github.com/blockscout/blockscout/pull/9155) - Allow bypassing avg block time in proxy implementation re-fetch ttl calculation +- [#9148](https://github.com/blockscout/blockscout/pull/9148) - Add `/api/v2/utils/decode-calldata` +- [#9145](https://github.com/blockscout/blockscout/pull/9145), [#9309](https://github.com/blockscout/blockscout/pull/9309) - Proxy for Account abstraction microservice +- [#9132](https://github.com/blockscout/blockscout/pull/9132) - Fetch token image from CoinGecko +- [#9131](https://github.com/blockscout/blockscout/pull/9131) - Merge addresses stage with address referencing +- [#9120](https://github.com/blockscout/blockscout/pull/9120) - Add GET and POST `/api/v2/smart-contracts/:address_hash/audit-reports` +- [#9072](https://github.com/blockscout/blockscout/pull/9072) - Add tracing by block logic for geth +- [#9185](https://github.com/blockscout/blockscout/pull/9185), [#9068](https://github.com/blockscout/blockscout/pull/9068) - New RPC API v1 endpoints +- [#9056](https://github.com/blockscout/blockscout/pull/9056) - Noves.fi API proxy + +### Fixes + +- [#9275](https://github.com/blockscout/blockscout/pull/9275) - Tx summary endpoint fixes +- [#9261](https://github.com/blockscout/blockscout/pull/9261) - Fix pending transactions sanitizer +- [#9253](https://github.com/blockscout/blockscout/pull/9253) - Don't fetch first trace for pending transactions +- [#9241](https://github.com/blockscout/blockscout/pull/9241) - Fix log decoding bug +- [#9234](https://github.com/blockscout/blockscout/pull/9234) - Add missing filters by non-pending transactions +- [#9229](https://github.com/blockscout/blockscout/pull/9229) - Add missing filter to txlist query +- [#9195](https://github.com/blockscout/blockscout/pull/9195) - API v1 allow multiple slashes in the path before "api" +- [#9187](https://github.com/blockscout/blockscout/pull/9187) - Fix Internal Server Error on request for nonexistent token instance +- [#9178](https://github.com/blockscout/blockscout/pull/9178) - Change internal txs tracer type to opcode for Hardhat node +- [#9173](https://github.com/blockscout/blockscout/pull/9173) - Exclude genesis block from average block time calculation +- [#9143](https://github.com/blockscout/blockscout/pull/9143) - Handle nil token_ids in token transfers on render +- [#9139](https://github.com/blockscout/blockscout/pull/9139) - TokenBalanceOnDemand fixes +- [#9125](https://github.com/blockscout/blockscout/pull/9125) - Fix Explorer.Chain.Cache.GasPriceOracle.merge_fees +- [#9124](https://github.com/blockscout/blockscout/pull/9124) - EIP-1167 display multiple sources of implementation +- [#9110](https://github.com/blockscout/blockscout/pull/9110) - Improve update_in in gas tracker +- [#9109](https://github.com/blockscout/blockscout/pull/9109) - Return current exchange rate in api/v2/stats +- [#9102](https://github.com/blockscout/blockscout/pull/9102) - Fix some log topics for Suave and Polygon Edge +- [#9075](https://github.com/blockscout/blockscout/pull/9075) - Fix fetching contract codes +- [#9073](https://github.com/blockscout/blockscout/pull/9073) - Allow payable function with output appear in the Read tab +- [#9069](https://github.com/blockscout/blockscout/pull/9069) - Fetch realtime coin balances only for addresses for which it has changed + +### Chore + +- [#9323](https://github.com/blockscout/blockscout/pull/9323) - Change index creation to concurrent +- [#9322](https://github.com/blockscout/blockscout/pull/9322) - Create repo setup actions +- [#9303](https://github.com/blockscout/blockscout/pull/9303) - Add workflow for Shibarium +- [#9233](https://github.com/blockscout/blockscout/pull/9233) - "cataloged" index on tokens table +- [#9198](https://github.com/blockscout/blockscout/pull/9198) - Make Postgres@15 default option +- [#9197](https://github.com/blockscout/blockscout/pull/9197) - Add `MARKET_HISTORY_FETCH_INTERVAL` env +- [#9196](https://github.com/blockscout/blockscout/pull/9196) - Compatibility with docker-compose 2.24 +- [#9193](https://github.com/blockscout/blockscout/pull/9193) - Equalize elixir stack versions +- [#9153](https://github.com/blockscout/blockscout/pull/9153) - Enhanced unfetched token balances index + +
+ Dependencies version bumps + +- [#9119](https://github.com/blockscout/blockscout/pull/9119) - Bump sass from 1.69.6 to 1.69.7 in /apps/block_scout_web/assets +- [#9126](https://github.com/blockscout/blockscout/pull/9126) - Bump follow-redirects from 1.14.8 to 1.15.4 in /apps/explorer +- [#9116](https://github.com/blockscout/blockscout/pull/9116) - Bump ueberauth from 0.10.5 to 0.10.7 +- [#9118](https://github.com/blockscout/blockscout/pull/9118) - Bump postcss from 8.4.32 to 8.4.33 in /apps/block_scout_web/assets +- [#9161](https://github.com/blockscout/blockscout/pull/9161) - Bump sass-loader from 13.3.3 to 14.0.0 in /apps/block_scout_web/assets +- [#9160](https://github.com/blockscout/blockscout/pull/9160) - Bump copy-webpack-plugin from 11.0.0 to 12.0.1 in /apps/block_scout_web/assets +- [#9165](https://github.com/blockscout/blockscout/pull/9165) - Bump sweetalert2 from 11.10.2 to 11.10.3 in /apps/block_scout_web/assets +- [#9163](https://github.com/blockscout/blockscout/pull/9163) - Bump mini-css-extract-plugin from 2.7.6 to 2.7.7 in /apps/block_scout_web/assets +- [#9159](https://github.com/blockscout/blockscout/pull/9159) - Bump @babel/preset-env from 7.23.7 to 7.23.8 in /apps/block_scout_web/assets +- [#9162](https://github.com/blockscout/blockscout/pull/9162) - Bump style-loader from 3.3.3 to 3.3.4 in /apps/block_scout_web/assets +- [#9164](https://github.com/blockscout/blockscout/pull/9164) - Bump css-loader from 6.8.1 to 6.9.0 in /apps/block_scout_web/assets +- [#8686](https://github.com/blockscout/blockscout/pull/8686) - Bump dialyxir from 1.4.1 to 1.4.2 +- [#8861](https://github.com/blockscout/blockscout/pull/8861) - Bump briefly from 51dfe7f to 4836ba3 +- [#9117](https://github.com/blockscout/blockscout/pull/9117) - Bump credo from 1.7.1 to 1.7.3 +- [#9222](https://github.com/blockscout/blockscout/pull/9222) - Bump dialyxir from 1.4.2 to 1.4.3 +- [#9219](https://github.com/blockscout/blockscout/pull/9219) - Bump sass from 1.69.7 to 1.70.0 in /apps/block_scout_web/assets +- [#9224](https://github.com/blockscout/blockscout/pull/9224) - Bump ex_cldr_numbers from 2.32.3 to 2.32.4 +- [#9220](https://github.com/blockscout/blockscout/pull/9220) - Bump copy-webpack-plugin from 12.0.1 to 12.0.2 in /apps/block_scout_web/assets +- [#9216](https://github.com/blockscout/blockscout/pull/9216) - Bump core-js from 3.35.0 to 3.35.1 in /apps/block_scout_web/assets +- [#9218](https://github.com/blockscout/blockscout/pull/9218) - Bump postcss-loader from 7.3.4 to 8.0.0 in /apps/block_scout_web/assets +- [#9223](https://github.com/blockscout/blockscout/pull/9223) - Bump plug_cowboy from 2.6.1 to 2.6.2 +- [#9217](https://github.com/blockscout/blockscout/pull/9217) - Bump css-loader from 6.9.0 to 6.9.1 in /apps/block_scout_web/assets +- [#9215](https://github.com/blockscout/blockscout/pull/9215) - Bump css-minimizer-webpack-plugin from 5.0.1 to 6.0.0 in /apps/block_scout_web/assets +- [#9221](https://github.com/blockscout/blockscout/pull/9221) - Bump autoprefixer from 10.4.16 to 10.4.17 in /apps/block_scout_web/assets + +
+ +## 6.0.0 + +### Features + +- [#9112](https://github.com/blockscout/blockscout/pull/9112) - Add specific url for eth_call +- [#9044](https://github.com/blockscout/blockscout/pull/9044) - Expand gas price oracle functionality + +### Fixes + +- [#9113](https://github.com/blockscout/blockscout/pull/9113) - Fix migrators cache updating +- [#9101](https://github.com/blockscout/blockscout/pull/9101) - Fix migration_finished? logic +- [#9062](https://github.com/blockscout/blockscout/pull/9062) - Fix blockscout-ens integration +- [#9061](https://github.com/blockscout/blockscout/pull/9061) - Arbitrum allow tx receipt gasUsedForL1 field +- [#8812](https://github.com/blockscout/blockscout/pull/8812) - Update existing tokens type if got transfer with higher type priority + +### Chore + +- [#9055](https://github.com/blockscout/blockscout/pull/9055) - Add ASC indices for logs, token transfers, transactions +- [#9038](https://github.com/blockscout/blockscout/pull/9038) - Token type filling migrations +- [#9009](https://github.com/blockscout/blockscout/pull/9009) - Index for block refetch_needed +- [#9007](https://github.com/blockscout/blockscout/pull/9007) - Drop logs type index +- [#9006](https://github.com/blockscout/blockscout/pull/9006) - Drop unused indexes on address_current_token_balances table +- [#9005](https://github.com/blockscout/blockscout/pull/9005) - Drop unused token_id column from token_transfers table and indexes based on this column +- [#9000](https://github.com/blockscout/blockscout/pull/9000) - Change log topic type in the DB to bytea +- [#8996](https://github.com/blockscout/blockscout/pull/8996) - Refine token transfers token ids index +- [#8776](https://github.com/blockscout/blockscout/pull/8776) - DB denormalization: block consensus and timestamp in transaction table + +
+ Dependencies version bumps + +- [#9059](https://github.com/blockscout/blockscout/pull/9059) - Bump redux from 5.0.0 to 5.0.1 in /apps/block_scout_web/assets +- [#9057](https://github.com/blockscout/blockscout/pull/9057) - Bump benchee from 1.2.0 to 1.3.0 +- [#9060](https://github.com/blockscout/blockscout/pull/9060) - Bump @amplitude/analytics-browser from 2.3.7 to 2.3.8 in /apps/block_scout_web/assets +- [#9084](https://github.com/blockscout/blockscout/pull/9084) - Bump @babel/preset-env from 7.23.6 to 7.23.7 in /apps/block_scout_web/assets +- [#9083](https://github.com/blockscout/blockscout/pull/9083) - Bump @babel/core from 7.23.6 to 7.23.7 in /apps/block_scout_web/assets +- [#9086](https://github.com/blockscout/blockscout/pull/9086) - Bump core-js from 3.34.0 to 3.35.0 in /apps/block_scout_web/assets +- [#9081](https://github.com/blockscout/blockscout/pull/9081) - Bump sweetalert2 from 11.10.1 to 11.10.2 in /apps/block_scout_web/assets +- [#9085](https://github.com/blockscout/blockscout/pull/9085) - Bump moment from 2.29.4 to 2.30.1 in /apps/block_scout_web/assets +- [#9087](https://github.com/blockscout/blockscout/pull/9087) - Bump postcss-loader from 7.3.3 to 7.3.4 in /apps/block_scout_web/assets +- [#9082](https://github.com/blockscout/blockscout/pull/9082) - Bump sass-loader from 13.3.2 to 13.3.3 in /apps/block_scout_web/assets +- [#9088](https://github.com/blockscout/blockscout/pull/9088) - Bump sass from 1.69.5 to 1.69.6 in /apps/block_scout_web/assets + +
+ +## 5.4.0-beta + +### Features + +- [#9018](https://github.com/blockscout/blockscout/pull/9018) - Add SmartContractRealtimeEventHandler +- [#8997](https://github.com/blockscout/blockscout/pull/8997) - Isolate throttable error count by request method +- [#8975](https://github.com/blockscout/blockscout/pull/8975) - Add EIP-4844 compatibility (not full support yet) +- [#8972](https://github.com/blockscout/blockscout/pull/8972) - BENS integration +- [#8960](https://github.com/blockscout/blockscout/pull/8960) - TRACE_BLOCK_RANGES env var +- [#8957](https://github.com/blockscout/blockscout/pull/8957) - Add Tx Interpreter Service integration +- [#8929](https://github.com/blockscout/blockscout/pull/8929) - Shibarium Bridge indexer and API v2 extension + +### Fixes + +- [#9039](https://github.com/blockscout/blockscout/pull/9039) - Fix tx input decoding in tx summary microservice request +- [#9035](https://github.com/blockscout/blockscout/pull/9035) - Handle Postgrex errors on NFT import +- [#9015](https://github.com/blockscout/blockscout/pull/9015) - Optimize NFT owner preload +- [#9013](https://github.com/blockscout/blockscout/pull/9013) - Speed up `Indexer.Fetcher.TokenInstance.LegacySanitize` +- [#8969](https://github.com/blockscout/blockscout/pull/8969) - Support legacy paging options for address transaction endpoint +- [#8965](https://github.com/blockscout/blockscout/pull/8965) - Set poll: false for internal transactions fetcher +- [#8955](https://github.com/blockscout/blockscout/pull/8955) - Remove daily balances updating from BlockReward fetcher +- [#8846](https://github.com/blockscout/blockscout/pull/8846) - Handle nil gas_price at address view + +### Chore + +- [#9094](https://github.com/blockscout/blockscout/pull/9094) - Improve exchange rates logging +- [#9014](https://github.com/blockscout/blockscout/pull/9014) - Decrease amount of NFT in address collection: 15 -> 9 +- [#8994](https://github.com/blockscout/blockscout/pull/8994) - Refactor transactions event preloads +- [#8991](https://github.com/blockscout/blockscout/pull/8991) - Manage DB queue target via runtime env var + +
+ Dependencies version bumps + +- [#8986](https://github.com/blockscout/blockscout/pull/8986) - Bump chart.js from 4.4.0 to 4.4.1 in /apps/block_scout_web/assets +- [#8982](https://github.com/blockscout/blockscout/pull/8982) - Bump ex_doc from 0.30.9 to 0.31.0 +- [#8987](https://github.com/blockscout/blockscout/pull/8987) - Bump @babel/preset-env from 7.23.5 to 7.23.6 in /apps/block_scout_web/assets +- [#8984](https://github.com/blockscout/blockscout/pull/8984) - Bump ecto_sql from 3.11.0 to 3.11.1 +- [#8988](https://github.com/blockscout/blockscout/pull/8988) - Bump core-js from 3.33.3 to 3.34.0 in /apps/block_scout_web/assets +- [#8980](https://github.com/blockscout/blockscout/pull/8980) - Bump exvcr from 0.14.4 to 0.15.0 +- [#8985](https://github.com/blockscout/blockscout/pull/8985) - Bump @babel/core from 7.23.5 to 7.23.6 in /apps/block_scout_web/assets +- [#9020](https://github.com/blockscout/blockscout/pull/9020) - Bump eslint-plugin-import from 2.29.0 to 2.29.1 in /apps/block_scout_web/assets +- [#9021](https://github.com/blockscout/blockscout/pull/9021) - Bump eslint from 8.55.0 to 8.56.0 in /apps/block_scout_web/assets +- [#9019](https://github.com/blockscout/blockscout/pull/9019) - Bump @amplitude/analytics-browser from 2.3.6 to 2.3.7 in /apps/block_scout_web/assets + +
+ +## 5.3.3-beta + +### Features + +- [#8966](https://github.com/blockscout/blockscout/pull/8966) - Add `ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS` +- [#8908](https://github.com/blockscout/blockscout/pull/8908) - Solidityscan report API endpoint +- [#8900](https://github.com/blockscout/blockscout/pull/8900) - Add Compound proxy contract pattern +- [#8611](https://github.com/blockscout/blockscout/pull/8611) - Implement sorting of smart contracts, address transactions + +### Fixes + +- [#8959](https://github.com/blockscout/blockscout/pull/8959) - Skip failed instances in Token Instance Owner migrator +- [#8924](https://github.com/blockscout/blockscout/pull/8924) - Delete invalid current token balances in OnDemand fetcher +- [#8922](https://github.com/blockscout/blockscout/pull/8922) - Allow call type to be in lowercase +- [#8917](https://github.com/blockscout/blockscout/pull/8917) - Proxy detection hotfix in API v2 +- [#8915](https://github.com/blockscout/blockscout/pull/8915) - smart-contract: delete embeds_many relation on replace +- [#8906](https://github.com/blockscout/blockscout/pull/8906) - Fix abi encoded string argument +- [#8898](https://github.com/blockscout/blockscout/pull/8898) - Enhance method decoding by candidates from DB +- [#8882](https://github.com/blockscout/blockscout/pull/8882) - Change order of proxy contracts patterns detection: existing popular EIPs to the top of the list +- [#8707](https://github.com/blockscout/blockscout/pull/8707) - Fix native coin exchange rate with `EXCHANGE_RATES_COINGECKO_COIN_ID` + +### Chore + +- [#8956](https://github.com/blockscout/blockscout/pull/8956) - Refine docker-compose config structure +- [#8911](https://github.com/blockscout/blockscout/pull/8911) - Set client_connection_check_interval for main Postgres DB in docker-compose setup + +
+ Dependencies version bumps + +- [#8863](https://github.com/blockscout/blockscout/pull/8863) - Bump core-js from 3.33.2 to 3.33.3 in /apps/block_scout_web/assets +- [#8864](https://github.com/blockscout/blockscout/pull/8864) - Bump @amplitude/analytics-browser from 2.3.3 to 2.3.5 in /apps/block_scout_web/assets +- [#8860](https://github.com/blockscout/blockscout/pull/8860) - Bump ecto_sql from 3.10.2 to 3.11.0 +- [#8896](https://github.com/blockscout/blockscout/pull/8896) - Bump httpoison from 2.2.0 to 2.2.1 +- [#8867](https://github.com/blockscout/blockscout/pull/8867) - Bump mixpanel-browser from 2.47.0 to 2.48.1 in /apps/block_scout_web/assets +- [#8865](https://github.com/blockscout/blockscout/pull/8865) - Bump eslint from 8.53.0 to 8.54.0 in /apps/block_scout_web/assets +- [#8866](https://github.com/blockscout/blockscout/pull/8866) - Bump sweetalert2 from 11.9.0 to 11.10.1 in /apps/block_scout_web/assets +- [#8897](https://github.com/blockscout/blockscout/pull/8897) - Bump prometheus from 4.10.0 to 4.11.0 +- [#8859](https://github.com/blockscout/blockscout/pull/8859) - Bump absinthe from 1.7.5 to 1.7.6 +- [#8858](https://github.com/blockscout/blockscout/pull/8858) - Bump ex_json_schema from 0.10.1 to 0.10.2 +- [#8943](https://github.com/blockscout/blockscout/pull/8943) - Bump postgrex from 0.17.3 to 0.17.4 +- [#8939](https://github.com/blockscout/blockscout/pull/8939) - Bump @babel/core from 7.23.3 to 7.23.5 in /apps/block_scout_web/assets +- [#8936](https://github.com/blockscout/blockscout/pull/8936) - Bump eslint from 8.54.0 to 8.55.0 in /apps/block_scout_web/assets +- [#8940](https://github.com/blockscout/blockscout/pull/8940) - Bump photoswipe from 5.4.2 to 5.4.3 in /apps/block_scout_web/assets +- [#8938](https://github.com/blockscout/blockscout/pull/8938) - Bump @babel/preset-env from 7.23.3 to 7.23.5 in /apps/block_scout_web/assets +- [#8935](https://github.com/blockscout/blockscout/pull/8935) - Bump @amplitude/analytics-browser from 2.3.5 to 2.3.6 in /apps/block_scout_web/assets +- [#8937](https://github.com/blockscout/blockscout/pull/8937) - Bump redux from 4.2.1 to 5.0.0 in /apps/block_scout_web/assets +- [#8942](https://github.com/blockscout/blockscout/pull/8942) - Bump gettext from 0.23.1 to 0.24.0 +- [#8934](https://github.com/blockscout/blockscout/pull/8934) - Bump @fortawesome/fontawesome-free from 6.4.2 to 6.5.1 in /apps/block_scout_web/assets +- [#8933](https://github.com/blockscout/blockscout/pull/8933) - Bump postcss from 8.4.31 to 8.4.32 in /apps/block_scout_web/assets + +
+ +## 5.3.2-beta + +### Features + +- [#8848](https://github.com/blockscout/blockscout/pull/8848) - Add MainPageRealtimeEventHandler +- [#8821](https://github.com/blockscout/blockscout/pull/8821) - Add new events to addresses channel: `eth_bytecode_db_lookup_started` and `smart_contract_was_not_verified` +- [#8795](https://github.com/blockscout/blockscout/pull/8795) - Disable catchup indexer by env +- [#8768](https://github.com/blockscout/blockscout/pull/8768) - Add possibility to search tokens by address hash +- [#8750](https://github.com/blockscout/blockscout/pull/8750) - Support new eth-bytecode-db request metadata fields +- [#8634](https://github.com/blockscout/blockscout/pull/8634) - API v2: NFT for address +- [#8609](https://github.com/blockscout/blockscout/pull/8609) - Change logs format to JSON; Add endpoint url to the block_scout_web logging +- [#8558](https://github.com/blockscout/blockscout/pull/8558) - Add CoinBalanceDailyUpdater + +### Fixes + +- [#8891](https://github.com/blockscout/blockscout/pull/8891) - Fix average block time +- [#8869](https://github.com/blockscout/blockscout/pull/8869) - Limit TokenBalance fetcher timeout +- [#8855](https://github.com/blockscout/blockscout/pull/8855) - All transactions count at top addresses page +- [#8836](https://github.com/blockscout/blockscout/pull/8836) - Safe token update +- [#8814](https://github.com/blockscout/blockscout/pull/8814) - Improve performance for EOA addresses in `/api/v2/addresses/{address_hash}` +- [#8813](https://github.com/blockscout/blockscout/pull/8813) - Force verify twin contracts on `/api/v2/import/smart-contracts/{address_hash}` +- [#8784](https://github.com/blockscout/blockscout/pull/8784) - Fix Indexer.Transform.Addresses for non-Suave setup +- [#8770](https://github.com/blockscout/blockscout/pull/8770) - Fix for eth_getbalance API v1 endpoint when requesting latest tag +- [#8765](https://github.com/blockscout/blockscout/pull/8765) - Fix for tvl update in market history when row already exists +- [#8759](https://github.com/blockscout/blockscout/pull/8759) - Gnosis safe proxy via singleton input +- [#8752](https://github.com/blockscout/blockscout/pull/8752) - Add `TOKEN_INSTANCE_OWNER_MIGRATION_ENABLED` env - [#8724](https://github.com/blockscout/blockscout/pull/8724) - Fix flaky account notifier test ### Chore +- [#8832](https://github.com/blockscout/blockscout/pull/8832) - Log more details in regards 413 error +- [#8807](https://github.com/blockscout/blockscout/pull/8807) - Smart-contract proxy detection refactoring +- [#8802](https://github.com/blockscout/blockscout/pull/8802) - Enable API v2 by default +- [#8742](https://github.com/blockscout/blockscout/pull/8742) - Merge rsk branch into the master branch - [#8728](https://github.com/blockscout/blockscout/pull/8728) - Remove repos_list (default value for ecto repos) from Explorer.ReleaseTasks
Dependencies version bumps + +- [#8727](https://github.com/blockscout/blockscout/pull/8727) - Bump browserify-sign from 4.2.1 to 4.2.2 in /apps/block_scout_web/assets +- [#8748](https://github.com/blockscout/blockscout/pull/8748) - Bump sweetalert2 from 11.7.32 to 11.9.0 in /apps/block_scout_web/assets +- [#8747](https://github.com/blockscout/blockscout/pull/8747) - Bump core-js from 3.33.1 to 3.33.2 in /apps/block_scout_web/assets +- [#8743](https://github.com/blockscout/blockscout/pull/8743) - Bump solc from 0.8.21 to 0.8.22 in /apps/explorer +- [#8745](https://github.com/blockscout/blockscout/pull/8745) - Bump tesla from 1.7.0 to 1.8.0 +- [#8749](https://github.com/blockscout/blockscout/pull/8749) - Bump sass from 1.69.4 to 1.69.5 in /apps/block_scout_web/assets +- [#8744](https://github.com/blockscout/blockscout/pull/8744) - Bump phoenix_ecto from 4.4.2 to 4.4.3 +- [#8746](https://github.com/blockscout/blockscout/pull/8746) - Bump floki from 0.35.1 to 0.35.2 +- [#8793](https://github.com/blockscout/blockscout/pull/8793) - Bump eslint from 8.52.0 to 8.53.0 in /apps/block_scout_web/assets +- [#8792](https://github.com/blockscout/blockscout/pull/8792) - Bump cldr_utils from 2.24.1 to 2.24.2 +- [#8787](https://github.com/blockscout/blockscout/pull/8787) - Bump ex_cldr_numbers from 2.32.2 to 2.32.3 +- [#8790](https://github.com/blockscout/blockscout/pull/8790) - Bump ex_abi from 0.6.3 to 0.6.4 +- [#8788](https://github.com/blockscout/blockscout/pull/8788) - Bump ex_cldr_units from 3.16.3 to 3.16.4 +- [#8827](https://github.com/blockscout/blockscout/pull/8827) - Bump @babel/core from 7.23.2 to 7.23.3 in /apps/block_scout_web/assets +- [#8823](https://github.com/blockscout/blockscout/pull/8823) - Bump benchee from 1.1.0 to 1.2.0 +- [#8826](https://github.com/blockscout/blockscout/pull/8826) - Bump luxon from 3.4.3 to 3.4.4 in /apps/block_scout_web/assets +- [#8824](https://github.com/blockscout/blockscout/pull/8824) - Bump httpoison from 2.1.0 to 2.2.0 +- [#8828](https://github.com/blockscout/blockscout/pull/8828) - Bump @babel/preset-env from 7.23.2 to 7.23.3 in /apps/block_scout_web/assets +- [#8825](https://github.com/blockscout/blockscout/pull/8825) - Bump solc from 0.8.22 to 0.8.23 in /apps/explorer +
## 5.3.1-beta @@ -34,7 +523,7 @@ - [#8708](https://github.com/blockscout/blockscout/pull/8708) - CoinBalanceHistory tab: show also tx with gasPrice & gasUsed > 0 - [#8706](https://github.com/blockscout/blockscout/pull/8706) - Add address name updating on contract re-verification - [#8705](https://github.com/blockscout/blockscout/pull/8705) - Fix sourcify enabled flag -- [#8695](https://github.com/blockscout/blockscout/pull/8695) - Don't override internal transaction error if it's present already +- [#8695](https://github.com/blockscout/blockscout/pull/8695), [#8755](https://github.com/blockscout/blockscout/pull/8755) - Don't override internal transaction error if it's present already - [#8685](https://github.com/blockscout/blockscout/pull/8685) - Fix db pool size exceeds Postgres max connections - [#8678](https://github.com/blockscout/blockscout/pull/8678) - Fix `is_verified` for `/addresses` and `/smart-contracts` @@ -422,6 +911,7 @@ - [#7958](https://github.com/blockscout/blockscout/pull/7958) - Bump ex_doc from 0.30.2 to 0.30.3 - [#7965](https://github.com/blockscout/blockscout/pull/7965) - Bump webpack from 5.88.1 to 5.88.2 in /apps/block_scout_web/assets - [#7972](https://github.com/blockscout/blockscout/pull/7972) - Bump word-wrap from 1.2.3 to 1.2.4 in /apps/block_scout_web/assets + ## 5.2.0-beta @@ -2189,8 +2679,8 @@ - [#3249](https://github.com/blockscout/blockscout/pull/3249) - Fix incorrect ABI decoding of address in tuple output - [#3237](https://github.com/blockscout/blockscout/pull/3237) - Refine contract method signature detection for read/write feature -- [#3235](https://github.com/blockscout/blockscout/pull/3235) - Fix coin supply api edpoint -- [#3233](https://github.com/blockscout/blockscout/pull/3233) - Fix for the contract verifiaction for solc 0.5 family with experimental features enabled +- [#3235](https://github.com/blockscout/blockscout/pull/3235) - Fix coin supply api endpoint +- [#3233](https://github.com/blockscout/blockscout/pull/3233) - Fix for the contract verification for solc 0.5 family with experimental features enabled - [#3231](https://github.com/blockscout/blockscout/pull/3231) - Improve search: unlimited number of searching results - [#3231](https://github.com/blockscout/blockscout/pull/3231) - Improve search: allow search with space - [#3231](https://github.com/blockscout/blockscout/pull/3231) - Improve search: order by token holders in descending order and token/contract name is ascending order @@ -2201,7 +2691,7 @@ - [#3326](https://github.com/blockscout/blockscout/pull/3326) - Chart smooth lines - [#3250](https://github.com/blockscout/blockscout/pull/3250) - Eliminate occurrences of obsolete env variable ETHEREUM_JSONRPC_JSON_RPC_TRANSPORT -- [#3240](https://github.com/blockscout/blockscout/pull/3240), [#3251](https://github.com/blockscout/blockscout/pull/3251) - various CSS imroving +- [#3240](https://github.com/blockscout/blockscout/pull/3240), [#3251](https://github.com/blockscout/blockscout/pull/3251) - various CSS improving - [f3a720](https://github.com/blockscout/blockscout/commit/2dd909c10a79b0bf4b7541a486be114152f3a720) - Make wobserver optional ## 3.3.1-beta @@ -2566,11 +3056,11 @@ fixed menu hovers in dark mode desktop view - [#2596](https://github.com/blockscout/blockscout/pull/2596) - support AuRa's empty step reward type - [#2588](https://github.com/blockscout/blockscout/pull/2588) - add verification submission comment - [#2505](https://github.com/blockscout/blockscout/pull/2505) - support POA Network emission rewards -- [#2581](https://github.com/blockscout/blockscout/pull/2581) - Add generic Map-like Cache behaviour and implementation +- [#2581](https://github.com/blockscout/blockscout/pull/2581) - Add generic Map-like Cache behavior and implementation - [#2561](https://github.com/blockscout/blockscout/pull/2561) - Add token's type to the response of tokenlist method - [#2555](https://github.com/blockscout/blockscout/pull/2555) - find and show decoding candidates for logs - [#2499](https://github.com/blockscout/blockscout/pull/2499) - import emission reward ranges -- [#2497](https://github.com/blockscout/blockscout/pull/2497) - Add generic Ordered Cache behaviour and implementation +- [#2497](https://github.com/blockscout/blockscout/pull/2497) - Add generic Ordered Cache behavior and implementation ### Fixes @@ -2602,7 +3092,7 @@ fixed menu hovers in dark mode desktop view - [#2617](https://github.com/blockscout/blockscout/pull/2617) - skip cache update if there are no blocks inserted - [#2611](https://github.com/blockscout/blockscout/pull/2611) - fix js dependency vulnerabilities - [#2594](https://github.com/blockscout/blockscout/pull/2594) - do not start genesis data fetching periodically -- [#2590](https://github.com/blockscout/blockscout/pull/2590) - restore backward compatablity with old releases +- [#2590](https://github.com/blockscout/blockscout/pull/2590) - restore backward compatibility with old releases - [#2577](https://github.com/blockscout/blockscout/pull/2577) - Need recompile column in the env vars table - [#2574](https://github.com/blockscout/blockscout/pull/2574) - limit request body in json rpc error - [#2566](https://github.com/blockscout/blockscout/pull/2566) - upgrade absinthe phoenix @@ -2626,7 +3116,7 @@ fixed menu hovers in dark mode desktop view - [#2559](https://github.com/blockscout/blockscout/pull/2559) - fix rsk total supply for empty exchange rate - [#2553](https://github.com/blockscout/blockscout/pull/2553) - Dark theme import to the end of sass - [#2550](https://github.com/blockscout/blockscout/pull/2550) - correctly encode decimal values for frontend -- [#2549](https://github.com/blockscout/blockscout/pull/2549) - Fix wrong colour of tooltip +- [#2549](https://github.com/blockscout/blockscout/pull/2549) - Fix wrong color of tooltip - [#2548](https://github.com/blockscout/blockscout/pull/2548) - CSS preload support in Firefox - [#2547](https://github.com/blockscout/blockscout/pull/2547) - do not show eth value if it's zero on the transaction overview page - [#2543](https://github.com/blockscout/blockscout/pull/2543) - do not hide search input during logs search @@ -2815,7 +3305,7 @@ fixed menu hovers in dark mode desktop view ### Chore -- [#2127](https://github.com/blockscout/blockscout/pull/2127) - use previouse chromedriver version +- [#2127](https://github.com/blockscout/blockscout/pull/2127) - use previous chromedriver version - [#2118](https://github.com/blockscout/blockscout/pull/2118) - show only the last decompiled contract - [#2255](https://github.com/blockscout/blockscout/pull/2255) - upgrade elixir version to 1.9.0 - [#2256](https://github.com/blockscout/blockscout/pull/2256) - use the latest version of chromedriver @@ -3050,7 +3540,7 @@ Reverting of synchronous block counter, implemented in #1848 ### Fixes -- [#1630](https://github.com/blockscout/blockscout/pull/1630) - (Fix) colour for release link in the footer +- [#1630](https://github.com/blockscout/blockscout/pull/1630) - (Fix) color for release link in the footer - [#1621](https://github.com/blockscout/blockscout/pull/1621) - Modify query to fetch failed contract creations - [#1614](https://github.com/blockscout/blockscout/pull/1614) - Do not fetch burn address token balance - [#1639](https://github.com/blockscout/blockscout/pull/1614) - Optimize token holder count updates when importing address current balances diff --git a/README.md b/README.md index 91130f13aaec..678d7f78d118 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,11 @@ Blockscout currently supports several hundred chains and rollups throughout the See the [project documentation](https://docs.blockscout.com/) for instructions: -- [Requirements](https://docs.blockscout.com/for-developers/information-and-settings/requirements) +- [Manual deployment](https://docs.blockscout.com/for-developers/deployment/manual-deployment-guide) +- [Docker-compose deployment](https://docs.blockscout.com/for-developers/deployment/docker-compose-deployment) +- [Kubernetes deployment](https://docs.blockscout.com/for-developers/deployment/kubernetes-deployment) +- [Manual deployment (backend + old UI)](https://docs.blockscout.com/for-developers/deployment/manual-old-ui) - [Ansible deployment](https://docs.blockscout.com/for-developers/ansible-deployment) -- [Manual deployment](https://docs.blockscout.com/for-developers/manual-deployment) - [ENV variables](https://docs.blockscout.com/for-developers/information-and-settings/env-variables) - [Configuration options](https://docs.blockscout.com/for-developers/configuration-options) diff --git a/apps/block_scout_web/.sobelow-conf b/apps/block_scout_web/.sobelow-conf index 82604d65bf53..1604d1f66daf 100644 --- a/apps/block_scout_web/.sobelow-conf +++ b/apps/block_scout_web/.sobelow-conf @@ -7,6 +7,7 @@ format: "compact", ignore: ["Config.Headers", "Config.CSWH", "XSS.SendResp", "XSS.Raw"], ignore_files: [ - "apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex" + "apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex", + "apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex" ] ] diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 859a02067788..4f74bccae256 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -7,17 +7,17 @@ "name": "blockscout", "license": "GPL-3.0", "dependencies": { - "@amplitude/analytics-browser": "^2.3.3", - "@fortawesome/fontawesome-free": "^6.4.2", + "@amplitude/analytics-browser": "^2.5.2", + "@fortawesome/fontawesome-free": "^6.5.1", "@tarekraafat/autocomplete.js": "^10.2.7", "@walletconnect/web3-provider": "^1.8.0", "assert": "^2.1.0", "bignumber.js": "^9.1.2", "bootstrap": "^4.6.0", - "chart.js": "^4.4.0", + "chart.js": "^4.4.2", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", - "core-js": "^3.33.2", + "core-js": "^3.36.0", "crypto-browserify": "^3.12.0", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", @@ -44,55 +44,55 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.4.3", + "luxon": "^3.4.4", "malihu-custom-scrollbar-plugin": "3.1.5", - "mixpanel-browser": "^2.47.0", - "moment": "^2.29.4", + "mixpanel-browser": "^2.49.0", + "moment": "^2.30.1", "nanomorph": "^5.4.0", "numeral": "^2.0.6", "os-browserify": "^0.3.0", "path-parser": "^6.1.0", "phoenix": "file:../../../deps/phoenix", "phoenix_html": "file:../../../deps/phoenix_html", - "photoswipe": "^5.4.2", + "photoswipe": "^5.4.3", "pikaday": "^1.8.2", "popper.js": "^1.14.7", "reduce-reducers": "^1.0.4", - "redux": "^4.2.1", + "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.9.0", + "sweetalert2": "^11.10.5", "urijs": "^1.19.11", "url": "^0.11.3", "util": "^0.12.5", "viewerjs": "^1.11.6", - "web3": "^1.10.3", + "web3": "^1.10.4", "web3modal": "^1.9.12", - "xss": "^1.0.14" + "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "autoprefixer": "^10.4.16", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "autoprefixer": "^10.4.18", "babel-loader": "^9.1.3", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "eslint": "^8.52.0", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^6.10.0", + "css-minimizer-webpack-plugin": "^6.0.0", + "eslint": "^8.57.0", "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.1.1", "file-loader": "^6.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mini-css-extract-plugin": "^2.7.6", - "postcss": "^8.4.31", - "postcss-loader": "^7.3.3", - "sass": "^1.69.5", - "sass-loader": "^13.3.2", - "style-loader": "^3.3.3", - "webpack": "^5.89.0", + "mini-css-extract-plugin": "^2.8.1", + "postcss": "^8.4.35", + "postcss-loader": "^8.1.1", + "sass": "^1.71.1", + "sass-loader": "^14.1.1", + "style-loader": "^3.3.4", + "webpack": "^5.90.3", "webpack-cli": "^5.1.4" }, "engines": { @@ -116,15 +116,15 @@ } }, "node_modules/@amplitude/analytics-browser": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.3.3.tgz", - "integrity": "sha512-we1tw7fn+yyf2waAyw/XqBOTEwIEY19qXJ5B9d1Xc1SKNxb6HzU5IpD3zbTKwZLlCGKbkVY6wI3JxuuoYIAI2w==", - "dependencies": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", - "@amplitude/plugin-page-view-tracking-browser": "^2.0.13", - "@amplitude/plugin-web-attribution-browser": "^2.0.13", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.5.2.tgz", + "integrity": "sha512-SjYjOEXO2it0dylVHv3n812ReA62c7GmxoeZCMMg9IFzgM54J4XIUTXYTDhQI/uGawjWZcsiuSqFgS/7LcwuSg==", + "dependencies": { + "@amplitude/analytics-client-common": "^2.1.0", + "@amplitude/analytics-core": "^2.2.1", + "@amplitude/analytics-types": "^2.5.0", + "@amplitude/plugin-page-view-tracking-browser": "^2.2.2", + "@amplitude/plugin-web-attribution-browser": "^2.1.3", "tslib": "^2.4.1" } }, @@ -134,13 +134,13 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/@amplitude/analytics-client-common": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.0.7.tgz", - "integrity": "sha512-2LqPMsY6ksS1OTda0EFPLUDFmIVTwU0bDN17+uY9WvpWTEWCAL616X2oNCQ6FTB9zWfCTr8X2I5jSS0y4rzPkw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.1.0.tgz", + "integrity": "sha512-wgZs7Orb3+ei7tBqxd5Ug3OzX19E0YpMkhHWFYesmpjBOW+Ng50rolyfB10rHjjeoMgiRc8eGDdnMH4+y7OQlA==", "dependencies": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-core": "^2.2.1", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" } }, @@ -155,11 +155,11 @@ "integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g==" }, "node_modules/@amplitude/analytics-core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.1.0.tgz", - "integrity": "sha512-a7/WUacF+R6J8NTYf93gaVd3OjOyF0db2K9Y6+uSZp/xIbsGyR/47WN1R3CYPm4IC9Y9/ukBAHd/p4FsNJbsNg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.2.1.tgz", + "integrity": "sha512-F4IrNzealRXnC3cPjkOoOEd68xsxuDsTeEGErAdxqWXNqV1x9oy1WM1lIKw2yGOyf6OZHidP/Vbp/BJK6B0Fmg==", "dependencies": { - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" } }, @@ -169,17 +169,17 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@amplitude/analytics-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.3.0.tgz", - "integrity": "sha512-/sMCgimMzDjGZDh5ekHUrwKVi4IJc8/AFUXIxEuJn4wd9QVmThWNeKfszA36z6Ue+UOHhEUEZwruXo3PwXWz/g==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.5.0.tgz", + "integrity": "sha512-aY69WxUvVlaCU+9geShjTsAYdUTvegEXH9i4WK/97kNbNLl4/7qUuIPe4hNireDeKLuQA9SA3H7TKynuNomDxw==" }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.0.13.tgz", - "integrity": "sha512-23X7tEublqLx6cs7o7KXyQ9UdfX0lSVNHDgurhuGdAV8yEeTdg9bRvVHGZETZtnbQj31avD7M2U31nXXaopX3A==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.2.2.tgz", + "integrity": "sha512-onsguK2fugi58vhW/NvuAqiQ7TG7IGmUKTg9Zfn39Y2IjudgT8N68LXz6esIY0XKQhZFHVsBT3pqYgEVK5I6Tg==", "dependencies": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-client-common": "^2.1.0", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" } }, @@ -189,13 +189,13 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@amplitude/plugin-web-attribution-browser": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.0.13.tgz", - "integrity": "sha512-oVsVxNk/twdcE5r5v+ALX3C443Q1NQGTaKNvbxyO/k3DiEGmKpgC8hSj9S5O7ZdrII63cS+u0IHevRXgmEnplg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.1.3.tgz", + "integrity": "sha512-CLTzgtewfgSLkwACkvPJ679Yr8HRiy1p8u9Nt5rvKlz3UeHXP7PqTQ/PC/nPp4AuaX30HB3xu4Eof8onWNm4nA==", "dependencies": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-client-common": "^2.1.0", + "@amplitude/analytics-core": "^2.2.1", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" } }, @@ -229,11 +229,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -241,28 +241,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -283,11 +283,11 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -308,25 +308,25 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", - "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -348,15 +348,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", - "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -371,14 +371,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", - "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "regexpu-core": "^5.3.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -435,12 +435,12 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -458,9 +458,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -488,9 +488,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "engines": { "node": ">=6.9.0" } @@ -513,13 +513,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -564,9 +564,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } @@ -580,9 +580,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "engines": { "node": ">=6.9.0" } @@ -602,24 +602,24 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -628,9 +628,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -639,9 +639,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -654,14 +654,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -670,6 +670,22 @@ "@babel/core": "^7.13.0" } }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -758,9 +774,9 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -773,9 +789,9 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -960,9 +976,9 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -975,9 +991,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", - "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -993,14 +1009,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1010,9 +1026,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1025,9 +1041,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1040,12 +1056,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1056,12 +1072,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -1073,18 +1089,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1096,13 +1111,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1112,9 +1127,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1127,12 +1142,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1143,9 +1158,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1158,9 +1173,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1174,12 +1189,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1190,9 +1205,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1206,12 +1221,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1221,13 +1237,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1238,9 +1254,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1254,9 +1270,9 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1269,9 +1285,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1285,9 +1301,9 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1300,12 +1316,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1316,12 +1332,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1333,13 +1349,13 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, @@ -1351,12 +1367,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1383,9 +1399,9 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1398,9 +1414,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1414,9 +1430,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1430,16 +1446,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.0.tgz", + "integrity": "sha512-y/yKMm7buHpFFXfxVFS4Vk1ToRJDilIa6fKRioB9Vjichv58TDGXTvqV0dN7plobAmTW5eSEGXDngE+Mm+uO+w==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1449,13 +1465,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1465,9 +1481,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1481,9 +1497,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1498,9 +1514,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1513,12 +1529,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1529,13 +1545,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1547,9 +1563,9 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1562,9 +1578,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1578,9 +1594,9 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1624,9 +1640,9 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1639,9 +1655,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1655,9 +1671,9 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1670,9 +1686,9 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1685,9 +1701,9 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1700,9 +1716,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1715,12 +1731,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1731,12 +1747,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1747,12 +1763,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1763,25 +1779,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", - "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.2", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1793,59 +1810,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.23.2", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.23.0", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.23.0", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-modules-systemjs": "^7.23.0", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.23.0", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1857,9 +1873,9 @@ } }, "node_modules/@babel/preset-env/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1873,13 +1889,13 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -1887,12 +1903,12 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -1930,32 +1946,32 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1963,11 +1979,11 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -2038,9 +2054,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -2067,9 +2083,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2106,9 +2122,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2157,14 +2173,14 @@ } }, "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", - "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz", + "integrity": "sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==", "dependencies": { - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" + "@noble/curves": "1.3.0", + "@noble/hashes": "1.3.3", + "@scure/bip32": "1.3.3", + "@scure/bip39": "1.2.2" } }, "node_modules/@ethersproject/abi": { @@ -2542,22 +2558,22 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", - "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", + "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==", "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2578,9 +2594,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -3311,9 +3327,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -3326,12 +3342,12 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@kurkle/color": { @@ -3345,20 +3361,20 @@ "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==" }, "node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "dependencies": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.3.3" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "engines": { "node": ">= 16" }, @@ -3402,33 +3418,33 @@ } }, "node_modules/@scure/base": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", - "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", + "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", "dependencies": { - "@noble/curves": "~1.1.0", - "@noble/hashes": "~1.3.1", - "@scure/base": "~1.1.0" + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", + "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", "dependencies": { - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -3451,6 +3467,18 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", + "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -3589,9 +3617,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/graceful-fs": { @@ -3604,9 +3632,9 @@ } }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -3677,9 +3705,9 @@ } }, "node_modules/@types/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dependencies": { "@types/node": "*" } @@ -4606,9 +4634,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "dev": true, "funding": [ { @@ -4625,9 +4653,9 @@ } ], "dependencies": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -4835,22 +4863,22 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.5.tgz", - "integrity": "sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.32.2" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -5209,9 +5237,9 @@ ] }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -5227,9 +5255,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -5480,9 +5508,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001549", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz", - "integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==", + "version": "1.0.30001593", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz", + "integrity": "sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==", "funding": [ { "type": "opencollective", @@ -5542,14 +5570,14 @@ } }, "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", "dependencies": { "@kurkle/color": "^0.3.0" }, "engines": { - "pnpm": ">=7" + "pnpm": ">=8" } }, "node_modules/chartjs-adapter-luxon": { @@ -5897,20 +5925,20 @@ } }, "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "dependencies": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -5921,9 +5949,9 @@ } }, "node_modules/core-js": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.2.tgz", - "integrity": "sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5931,11 +5959,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", - "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -5960,21 +5988,29 @@ } }, "node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { - "import-fresh": "^3.2.1", + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/cosmiconfig/node_modules/argparse": { @@ -6189,31 +6225,31 @@ } }, "node_modules/css-declaration-sorter": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", - "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", + "integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==", "dev": true, "engines": { - "node": "^10 || ^12 || >=14" + "node": "^14 || ^16 || >=18" }, "peerDependencies": { "postcss": "^8.0.9" } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">= 12.13.0" @@ -6223,7 +6259,16 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-loader/node_modules/semver": { @@ -6242,20 +6287,20 @@ } }, "node_modules/css-minimizer-webpack-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", - "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-6.0.0.tgz", + "integrity": "sha512-BLpR9CCDkKvhO3i0oZQgad6v9pCxUuhSc5RT6iUEy9M8hBXi4TJb5vqF2GQ2deqYHmRi3O6IR9hgAZQWg0EBwA==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "cssnano": "^6.0.1", - "jest-worker": "^29.4.3", - "postcss": "^8.4.24", - "schema-utils": "^4.0.1", - "serialize-javascript": "^6.0.1" + "@jridgewell/trace-mapping": "^0.3.21", + "cssnano": "^6.0.3", + "jest-worker": "^29.7.0", + "postcss": "^8.4.33", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -6295,13 +6340,13 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -6393,13 +6438,13 @@ "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" }, "node_modules/cssnano": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.1.tgz", - "integrity": "sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz", + "integrity": "sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==", "dev": true, "dependencies": { - "cssnano-preset-default": "^6.0.1", - "lilconfig": "^2.1.0" + "cssnano-preset-default": "^6.0.3", + "lilconfig": "^3.0.0" }, "engines": { "node": "^14 || ^16 || >=18.0" @@ -6409,62 +6454,62 @@ "url": "https://opencollective.com/cssnano" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/cssnano-preset-default": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.1.tgz", - "integrity": "sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ==", - "dev": true, - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^4.0.0", - "postcss-calc": "^9.0.0", - "postcss-colormin": "^6.0.0", - "postcss-convert-values": "^6.0.0", - "postcss-discard-comments": "^6.0.0", - "postcss-discard-duplicates": "^6.0.0", - "postcss-discard-empty": "^6.0.0", - "postcss-discard-overridden": "^6.0.0", - "postcss-merge-longhand": "^6.0.0", - "postcss-merge-rules": "^6.0.1", - "postcss-minify-font-values": "^6.0.0", - "postcss-minify-gradients": "^6.0.0", - "postcss-minify-params": "^6.0.0", - "postcss-minify-selectors": "^6.0.0", - "postcss-normalize-charset": "^6.0.0", - "postcss-normalize-display-values": "^6.0.0", - "postcss-normalize-positions": "^6.0.0", - "postcss-normalize-repeat-style": "^6.0.0", - "postcss-normalize-string": "^6.0.0", - "postcss-normalize-timing-functions": "^6.0.0", - "postcss-normalize-unicode": "^6.0.0", - "postcss-normalize-url": "^6.0.0", - "postcss-normalize-whitespace": "^6.0.0", - "postcss-ordered-values": "^6.0.0", - "postcss-reduce-initial": "^6.0.0", - "postcss-reduce-transforms": "^6.0.0", - "postcss-svgo": "^6.0.0", - "postcss-unique-selectors": "^6.0.0" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.3.tgz", + "integrity": "sha512-4y3H370aZCkT9Ev8P4SO4bZbt+AExeKhh8wTbms/X7OLDo5E7AYUUy6YPxa/uF5Grf+AJwNcCnxKhZynJ6luBA==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^7.1.1", + "cssnano-utils": "^4.0.1", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.0.2", + "postcss-convert-values": "^6.0.2", + "postcss-discard-comments": "^6.0.1", + "postcss-discard-duplicates": "^6.0.1", + "postcss-discard-empty": "^6.0.1", + "postcss-discard-overridden": "^6.0.1", + "postcss-merge-longhand": "^6.0.2", + "postcss-merge-rules": "^6.0.3", + "postcss-minify-font-values": "^6.0.1", + "postcss-minify-gradients": "^6.0.1", + "postcss-minify-params": "^6.0.2", + "postcss-minify-selectors": "^6.0.2", + "postcss-normalize-charset": "^6.0.1", + "postcss-normalize-display-values": "^6.0.1", + "postcss-normalize-positions": "^6.0.1", + "postcss-normalize-repeat-style": "^6.0.1", + "postcss-normalize-string": "^6.0.1", + "postcss-normalize-timing-functions": "^6.0.1", + "postcss-normalize-unicode": "^6.0.2", + "postcss-normalize-url": "^6.0.1", + "postcss-normalize-whitespace": "^6.0.1", + "postcss-ordered-values": "^6.0.1", + "postcss-reduce-initial": "^6.0.2", + "postcss-reduce-transforms": "^6.0.1", + "postcss-svgo": "^6.0.2", + "postcss-unique-selectors": "^6.0.2" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/cssnano-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.0.tgz", - "integrity": "sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.1.tgz", + "integrity": "sha512-6qQuYDqsGoiXssZ3zct6dcMxiqfT6epy7x4R0TQJadd4LWO3sPR6JH6ZByOvVLoZ6EdwPGgd7+DR1EmX3tiXQQ==", "dev": true, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/csso": { @@ -6787,18 +6832,6 @@ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6903,9 +6936,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.555", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.555.tgz", - "integrity": "sha512-k1wGC7UXDTyCWcONkEMRG/w6Jvrxi+SVEU+IeqUKUKjv2lGJ1b+jf1mqrloyxVTG5WYYjNQ+F6+Cb1fGrLvNcA==" + "version": "1.4.692", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", + "integrity": "sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -7017,6 +7050,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/envinfo": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", @@ -7149,13 +7191,14 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -7290,16 +7333,16 @@ } }, "node_modules/eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -7440,9 +7483,9 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", - "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { "array-includes": "^3.1.7", @@ -7461,7 +7504,7 @@ "object.groupby": "^1.0.1", "object.values": "^1.1.7", "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -7869,6 +7912,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -8346,9 +8408,9 @@ } }, "node_modules/ethereumjs-util/node_modules/@types/bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "dependencies": { "@types/node": "*" } @@ -8459,6 +8521,15 @@ "npm": ">=3" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/eventemitter3": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", @@ -8693,9 +8764,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -8952,9 +9023,9 @@ } }, "node_modules/fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, "engines": { "node": "*" @@ -9203,28 +9274,29 @@ } }, "node_modules/globby": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", - "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", + "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "@sindresorhus/merge-streams": "^1.0.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "node_modules/globby/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { "node": ">=12" @@ -9233,6 +9305,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/good-listener": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", @@ -9510,9 +9594,9 @@ } }, "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -9615,9 +9699,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" @@ -11891,9 +11975,9 @@ } }, "node_modules/jiti": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", - "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -12326,12 +12410,12 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/lines-and-columns": { @@ -12522,9 +12606,9 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -12801,12 +12885,13 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", "dev": true, "dependencies": { - "schema-utils": "^4.0.0" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, "engines": { "node": ">= 12.13.0" @@ -12869,9 +12954,9 @@ } }, "node_modules/mixpanel-browser": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", - "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.49.0.tgz", + "integrity": "sha512-RZJCO7XXuuHBAWG5fd9Mavz994M7v7W3Qiaq8NzmN631pa4BQ0vNZQtRFqKcCCOBn4xqOZbX2GkuC7ZkQoL4cQ==" }, "node_modules/mkdirp": { "version": "3.0.1", @@ -12905,9 +12990,9 @@ "integrity": "sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw==" }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { "node": "*" } @@ -12967,9 +13052,9 @@ "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -13077,9 +13162,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -13510,15 +13595,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -13556,9 +13632,9 @@ "link": true }, "node_modules/photoswipe": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.2.tgz", - "integrity": "sha512-z5hr36nAIPOZbHJPbCJ/mQ3+ZlizttF9za5gKXKH/us1k4KNHaRbC63K1Px5sVVKUtGb/2+ixHpKqtwl0WAwvA==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.3.tgz", + "integrity": "sha512-9UC6oJBK4oXFZ5HcdlcvGkfEHsVrmE4csUdCQhEjHYb3PvPLO3PG7UhnPuOgjxwmhq5s17Un5NUdum01LgBDng==", "engines": { "node": ">= 0.12.0" } @@ -13718,9 +13794,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -13737,7 +13813,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -13762,12 +13838,12 @@ } }, "node_modules/postcss-colormin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.0.tgz", - "integrity": "sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.2.tgz", + "integrity": "sha512-TXKOxs9LWcdYo5cgmcSHPkyrLAh86hX1ijmyy6J8SbOhyv6ua053M3ZAM/0j44UsnQNIWdl8gb5L7xX2htKeLw==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0", "colord": "^2.9.1", "postcss-value-parser": "^4.2.0" @@ -13776,99 +13852,108 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-convert-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.0.tgz", - "integrity": "sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.2.tgz", + "integrity": "sha512-aeBmaTnGQ+NUSVQT8aY0sKyAD/BaLJenEKZ03YK0JnDE1w1Rr8XShoxdal2V2H26xTJKr3v5haByOhJuyT4UYw==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-discard-comments": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.0.tgz", - "integrity": "sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.1.tgz", + "integrity": "sha512-f1KYNPtqYLUeZGCHQPKzzFtsHaRuECe6jLakf/RjSRqvF5XHLZnM2+fXLhb8Qh/HBFHs3M4cSLb1k3B899RYIg==", "dev": true, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-discard-duplicates": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.0.tgz", - "integrity": "sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.1.tgz", + "integrity": "sha512-1hvUs76HLYR8zkScbwyJ8oJEugfPV+WchpnA+26fpJ7Smzs51CzGBHC32RS03psuX/2l0l0UKh2StzNxOrKCYg==", "dev": true, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-discard-empty": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.0.tgz", - "integrity": "sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.1.tgz", + "integrity": "sha512-yitcmKwmVWtNsrrRqGJ7/C0YRy53i0mjexBDQ9zYxDwTWVBgbU4+C9jIZLmQlTDT9zhml+u0OMFJh8+31krmOg==", "dev": true, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-discard-overridden": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz", - "integrity": "sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.1.tgz", + "integrity": "sha512-qs0ehZMMZpSESbRkw1+inkf51kak6OOzNRaoLd/U7Fatp0aN2HQ1rxGOrJvYcRAN9VpX8kUF13R2ofn8OlvFVA==", "dev": true, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-loader": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", - "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, "dependencies": { - "cosmiconfig": "^8.2.0", - "jiti": "^1.18.2", - "semver": "^7.3.8" + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/postcss-loader/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13881,43 +13966,43 @@ } }, "node_modules/postcss-merge-longhand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz", - "integrity": "sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.2.tgz", + "integrity": "sha512-+yfVB7gEM8SrCo9w2lCApKIEzrTKl5yS1F4yGhV3kSim6JzbfLGJyhR1B6X+6vOT0U33Mgx7iv4X9MVWuaSAfw==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.0.0" + "stylehacks": "^6.0.2" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-merge-rules": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.1.tgz", - "integrity": "sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.3.tgz", + "integrity": "sha512-yfkDqSHGohy8sGYIJwBmIGDv4K4/WrJPX355XrxQb/CSsT4Kc/RxDi6akqn5s9bap85AWgv21ArcUWwWdGNSHA==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.0", - "postcss-selector-parser": "^6.0.5" + "cssnano-utils": "^4.0.1", + "postcss-selector-parser": "^6.0.15" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-minify-font-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.0.tgz", - "integrity": "sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.1.tgz", + "integrity": "sha512-tIwmF1zUPoN6xOtA/2FgVk1ZKrLcCvE0dpZLtzyyte0j9zUeB8RTbCqrHZGjJlxOvNWKMYtunLrrl7HPOiR46w==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -13926,56 +14011,56 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-minify-gradients": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.0.tgz", - "integrity": "sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.1.tgz", + "integrity": "sha512-M1RJWVjd6IOLPl1hYiOd5HQHgpp6cvJVLrieQYS9y07Yo8itAr6jaekzJphaJFR0tcg4kRewCk3kna9uHBxn/w==", "dev": true, "dependencies": { "colord": "^2.9.1", - "cssnano-utils": "^4.0.0", + "cssnano-utils": "^4.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-minify-params": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.0.tgz", - "integrity": "sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.2.tgz", + "integrity": "sha512-zwQtbrPEBDj+ApELZ6QylLf2/c5zmASoOuA4DzolyVGdV38iR2I5QRMsZcHkcdkZzxpN8RS4cN7LPskOkTwTZw==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^4.0.0", + "browserslist": "^4.22.2", + "cssnano-utils": "^4.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-minify-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.0.tgz", - "integrity": "sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.2.tgz", + "integrity": "sha512-0b+m+w7OAvZejPQdN2GjsXLv5o0jqYHX3aoV0e7RBKPCsB7TYG5KKWBFhGnB/iP3213Ts8c5H4wLPLMm7z28Sg==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.5" + "postcss-selector-parser": "^6.0.15" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-modules-extract-imports": { @@ -13991,9 +14076,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -14008,9 +14093,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -14038,21 +14123,21 @@ } }, "node_modules/postcss-normalize-charset": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.0.tgz", - "integrity": "sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.1.tgz", + "integrity": "sha512-aW5LbMNRZ+oDV57PF9K+WI1Z8MPnF+A8qbajg/T8PP126YrGX1f9IQx21GI2OlGz7XFJi/fNi0GTbY948XJtXg==", "dev": true, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-display-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.0.tgz", - "integrity": "sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.1.tgz", + "integrity": "sha512-mc3vxp2bEuCb4LgCcmG1y6lKJu1Co8T+rKHrcbShJwUmKJiEl761qb/QQCfFwlrvSeET3jksolCR/RZuMURudw==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14061,13 +14146,13 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-positions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.0.tgz", - "integrity": "sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.1.tgz", + "integrity": "sha512-HRsq8u/0unKNvm0cvwxcOUEcakFXqZ41fv3FOdPn916XFUrympjr+03oaLkuZENz3HE9RrQE9yU0Xv43ThWjQg==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14076,13 +14161,13 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.0.tgz", - "integrity": "sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.1.tgz", + "integrity": "sha512-Gbb2nmCy6tTiA7Sh2MBs3fj9W8swonk6lw+dFFeQT68B0Pzwp1kvisJQkdV6rbbMSd9brMlS8I8ts52tAGWmGQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14091,13 +14176,13 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-string": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.0.tgz", - "integrity": "sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.1.tgz", + "integrity": "sha512-5Fhx/+xzALJD9EI26Aq23hXwmv97Zfy2VFrt5PLT8lAhnBIZvmaT5pQk+NuJ/GWj/QWaKSKbnoKDGLbV6qnhXg==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14106,13 +14191,13 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-timing-functions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.0.tgz", - "integrity": "sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.1.tgz", + "integrity": "sha512-4zcczzHqmCU7L5dqTB9rzeqPWRMc0K2HoR+Bfl+FSMbqGBUcP5LRfgcH4BdRtLuzVQK1/FHdFoGT3F7rkEnY+g==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14121,29 +14206,29 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-unicode": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.0.tgz", - "integrity": "sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.2.tgz", + "integrity": "sha512-Ff2VdAYCTGyMUwpevTZPZ4w0+mPjbZzLLyoLh/RMpqUqeQKZ+xMm31hkxBavDcGKcxm6ACzGk0nBfZ8LZkStKA==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.0.tgz", - "integrity": "sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.1.tgz", + "integrity": "sha512-jEXL15tXSvbjm0yzUV7FBiEXwhIa9H88JOXDGQzmcWoB4mSjZIsmtto066s2iW9FYuIrIF4k04HA2BKAOpbsaQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14152,13 +14237,13 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-normalize-whitespace": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.0.tgz", - "integrity": "sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.1.tgz", + "integrity": "sha512-76i3NpWf6bB8UHlVuLRxG4zW2YykF9CTEcq/9LGAiz2qBuX5cBStadkk0jSkg9a9TCIXbMQz7yzrygKoCW9JuA==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14167,45 +14252,45 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-ordered-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.0.tgz", - "integrity": "sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.1.tgz", + "integrity": "sha512-XXbb1O/MW9HdEhnBxitZpPFbIvDgbo9NK4c/5bOfiKpnIGZDoL2xd7/e6jW5DYLsWxBbs+1nZEnVgnjnlFViaA==", "dev": true, "dependencies": { - "cssnano-utils": "^4.0.0", + "cssnano-utils": "^4.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-reduce-initial": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", - "integrity": "sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.2.tgz", + "integrity": "sha512-YGKalhNlCLcjcLvjU5nF8FyeCTkCO5UtvJEt0hrPZVCTtRLSOH4z00T1UntQPj4dUmIYZgMj8qK77JbSX95hSw==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-reduce-transforms": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.0.tgz", - "integrity": "sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.1.tgz", + "integrity": "sha512-fUbV81OkUe75JM+VYO1gr/IoA2b/dRiH6HvMwhrIBSUrxq3jNZQZitSnugcTLDi1KkQh1eR/zi+iyxviUNBkcQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0" @@ -14214,13 +14299,13 @@ "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -14231,34 +14316,34 @@ } }, "node_modules/postcss-svgo": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.0.tgz", - "integrity": "sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.2.tgz", + "integrity": "sha512-IH5R9SjkTkh0kfFOQDImyy1+mTCb+E830+9SV1O+AaDcoHTvfsvt6WwJeo7KwcHbFnevZVCsXhDmjFiGVuwqFQ==", "dev": true, "dependencies": { "postcss-value-parser": "^4.2.0", - "svgo": "^3.0.2" + "svgo": "^3.2.0" }, "engines": { "node": "^14 || ^16 || >= 18" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-unique-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.0.tgz", - "integrity": "sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.2.tgz", + "integrity": "sha512-8IZGQ94nechdG7Y9Sh9FlIY2b4uS8/k8kdKRX040XHsS3B6d1HrJAkXrBSsSu4SuARruSsUjW3nlSw8BHkaAYQ==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.5" + "postcss-selector-parser": "^6.0.15" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/postcss-value-parser": { @@ -14784,12 +14869,9 @@ "integrity": "sha512-Mb2WZ2bJF597exiqX7owBzrqJ74DHLK3yOQjCyPAaNifRncE8OD0wFIuoMhXxTnHK07+8zZ2SJEKy/qtiyR7vw==" }, "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/regenerate": { "version": "1.4.2", @@ -15171,9 +15253,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -15188,29 +15270,29 @@ } }, "node_modules/sass-loader": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", - "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", "dev": true, "dependencies": { "neo-async": "^2.6.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "fibers": ">= 3.1.0", + "@rspack/core": "0.x || 1.x", "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", "sass-embedded": "*", "webpack": "^5.0.0" }, "peerDependenciesMeta": { - "fibers": { + "@rspack/core": { "optional": true }, "node-sass": { @@ -15221,6 +15303,9 @@ }, "sass-embedded": { "optional": true + }, + "webpack": { + "optional": true } } }, @@ -15246,9 +15331,9 @@ } }, "node_modules/schema-utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.1.0.tgz", - "integrity": "sha512-Jw+GZVbP5IggB2WAn6UHI02LBwGmsIeYN/lNbSMZyDziQ7jmtAUrqKqDja+W89YHVs+KL/3IkIMltAklqB1vAw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", @@ -15385,9 +15470,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -15874,9 +15959,9 @@ } }, "node_modules/style-loader": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", - "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", "dev": true, "engines": { "node": ">= 12.13.0" @@ -15919,19 +16004,19 @@ } }, "node_modules/stylehacks": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.0.tgz", - "integrity": "sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.2.tgz", + "integrity": "sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==", "dev": true, "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" + "browserslist": "^4.22.2", + "postcss-selector-parser": "^6.0.15" }, "engines": { "node": "^14 || ^16 || >=18.0" }, "peerDependencies": { - "postcss": "^8.2.15" + "postcss": "^8.4.31" } }, "node_modules/supports-color": { @@ -15957,15 +16042,16 @@ } }, "node_modules/svgo": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz", - "integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", "dev": true, "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", - "css-tree": "^2.2.1", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, @@ -16070,9 +16156,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.9.0.tgz", - "integrity": "sha512-PA3qinKZMNGAhA+AUu2wU7yQOpeZCgOaYWRcg26f4cZN6f7M9iPBuobsxOhR9EHs7ihUIxT6vhAMiB4kcmk1SA==", + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.10.5.tgz", + "integrity": "sha512-q9eE3EKhMcpIDU/Xcz7z5lk8axCGkgxwK47gXGrrfncnBJWxHPPHnBVAjfsVXcTt8Yi8U6HNEcBRSu+qGeyFdA==", "funding": { "type": "individual", "url": "https://github.com/sponsors/limonte" @@ -16146,13 +16232,13 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/terser": { - "version": "5.16.9", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.9.tgz", - "integrity": "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -16164,16 +16250,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", - "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.5" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -16198,9 +16284,9 @@ } }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -16330,9 +16416,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", @@ -16565,6 +16651,18 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -16806,27 +16904,27 @@ } }, "node_modules/web3": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.3.tgz", - "integrity": "sha512-DgUdOOqC/gTqW+VQl1EdPxrVRPB66xVNtuZ5KD4adVBtko87hkgM8BTZ0lZ8IbUfnQk6DyjcDujMiH3oszllAw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.4.tgz", + "integrity": "sha512-kgJvQZjkmjOEKimx/tJQsqWfRDPTTcBfYPa9XletxuHLpHcXdx67w8EFn5AW3eVxCutE9dTVHgGa9VYe8vgsEA==", "hasInstallScript": true, "dependencies": { - "web3-bzz": "1.10.3", - "web3-core": "1.10.3", - "web3-eth": "1.10.3", - "web3-eth-personal": "1.10.3", - "web3-net": "1.10.3", - "web3-shh": "1.10.3", - "web3-utils": "1.10.3" + "web3-bzz": "1.10.4", + "web3-core": "1.10.4", + "web3-eth": "1.10.4", + "web3-eth-personal": "1.10.4", + "web3-net": "1.10.4", + "web3-shh": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-bzz": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.3.tgz", - "integrity": "sha512-XDIRsTwekdBXtFytMpHBuun4cK4x0ZMIDXSoo1UVYp+oMyZj07c7gf7tNQY5qZ/sN+CJIas4ilhN25VJcjSijQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.4.tgz", + "integrity": "sha512-ZZ/X4sJ0Uh2teU9lAGNS8EjveEppoHNQiKlOXAjedsrdWuaMErBPdLQjXfcrYvN6WM6Su9PMsAxf3FXXZ+HwQw==", "hasInstallScript": true, "dependencies": { "@types/node": "^12.12.6", @@ -16843,53 +16941,53 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/web3-core": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.3.tgz", - "integrity": "sha512-Vbk0/vUNZxJlz3RFjAhNNt7qTpX8yE3dn3uFxfX5OHbuon5u65YEOd3civ/aQNW745N0vGUlHFNxxmn+sG9DIw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.4.tgz", + "integrity": "sha512-B6elffYm81MYZDTrat7aEhnhdtVE3lDBUZft16Z8awYMZYJDbnykEbJVS+l3mnA7AQTnSDr/1MjWofGDLBJPww==", "dependencies": { "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-requestmanager": "1.10.3", - "web3-utils": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-requestmanager": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-helpers": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.3.tgz", - "integrity": "sha512-Yv7dQC3B9ipOc5sWm3VAz1ys70Izfzb8n9rSiQYIPjpqtJM+3V4EeK6ghzNR6CO2es0+Yu9CtCkw0h8gQhrTxA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.4.tgz", + "integrity": "sha512-r+L5ylA17JlD1vwS8rjhWr0qg7zVoVMDvWhajWA5r5+USdh91jRUYosp19Kd1m2vE034v7Dfqe1xYRoH2zvG0g==", "dependencies": { - "web3-eth-iban": "1.10.3", - "web3-utils": "1.10.3" + "web3-eth-iban": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-method": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.3.tgz", - "integrity": "sha512-VZ/Dmml4NBmb0ep5PTSg9oqKoBtG0/YoMPei/bq/tUdlhB2dMB79sbeJPwx592uaV0Vpk7VltrrrBv5hTM1y4Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.4.tgz", + "integrity": "sha512-uZTb7flr+Xl6LaDsyTeE2L1TylokCJwTDrIVfIfnrGmnwLc6bmTWCCrm71sSrQ0hqs6vp/MKbQYIYqUN0J8WyA==", "dependencies": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-utils": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-promievent": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.3.tgz", - "integrity": "sha512-HgjY+TkuLm5uTwUtaAfkTgRx/NzMxvVradCi02gy17NxDVdg/p6svBHcp037vcNpkuGeFznFJgULP+s2hdVgUQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.4.tgz", + "integrity": "sha512-2de5WnJQ72YcIhYwV/jHLc4/cWJnznuoGTJGD29ncFQHAfwW/MItHFSVKPPA5v8AhJe+r6y4Y12EKvZKjQVBvQ==", "dependencies": { "eventemitter3": "4.0.4" }, @@ -16898,36 +16996,36 @@ } }, "node_modules/web3-core-requestmanager": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.3.tgz", - "integrity": "sha512-VT9sKJfgM2yBOIxOXeXiDuFMP4pxzF6FT+y8KTLqhDFHkbG3XRe42Vm97mB/IvLQCJOmokEjl3ps8yP1kbggyw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.4.tgz", + "integrity": "sha512-vqP6pKH8RrhT/2MoaU+DY/OsYK9h7HmEBNCdoMj+4ZwujQtw/Mq2JifjwsJ7gits7Q+HWJwx8q6WmQoVZAWugg==", "dependencies": { "util": "^0.12.5", - "web3-core-helpers": "1.10.3", - "web3-providers-http": "1.10.3", - "web3-providers-ipc": "1.10.3", - "web3-providers-ws": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-providers-http": "1.10.4", + "web3-providers-ipc": "1.10.4", + "web3-providers-ws": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-subscriptions": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.3.tgz", - "integrity": "sha512-KW0Mc8sgn70WadZu7RjQ4H5sNDJ5Lx8JMI3BWos+f2rW0foegOCyWhRu33W1s6ntXnqeBUw5rRCXZRlA3z+HNA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.4.tgz", + "integrity": "sha512-o0lSQo/N/f7/L76C0HV63+S54loXiE9fUPfHFcTtpJRQNDBVsSDdWRdePbWwR206XlsBqD5VHApck1//jEafTw==", "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core/node_modules/@types/bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "dependencies": { "@types/node": "*" } @@ -16938,43 +17036,43 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/web3-eth": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.3.tgz", - "integrity": "sha512-Uk1U2qGiif2mIG8iKu23/EQJ2ksB1BQXy3wF3RvFuyxt8Ft9OEpmGlO7wOtAyJdoKzD5vcul19bJpPcWSAYZhA==", - "dependencies": { - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-eth-accounts": "1.10.3", - "web3-eth-contract": "1.10.3", - "web3-eth-ens": "1.10.3", - "web3-eth-iban": "1.10.3", - "web3-eth-personal": "1.10.3", - "web3-net": "1.10.3", - "web3-utils": "1.10.3" + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.4.tgz", + "integrity": "sha512-Sql2kYKmgt+T/cgvg7b9ce24uLS7xbFrxE4kuuor1zSCGrjhTJ5rRNG8gTJUkAJGKJc7KgnWmgW+cOfMBPUDSA==", + "dependencies": { + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-eth-accounts": "1.10.4", + "web3-eth-contract": "1.10.4", + "web3-eth-ens": "1.10.4", + "web3-eth-iban": "1.10.4", + "web3-eth-personal": "1.10.4", + "web3-net": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-abi": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.3.tgz", - "integrity": "sha512-O8EvV67uhq0OiCMekqYsDtb6FzfYzMXT7VMHowF8HV6qLZXCGTdB/NH4nJrEh2mFtEwVdS6AmLFJAQd2kVyoMQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.4.tgz", + "integrity": "sha512-cZ0q65eJIkd/jyOlQPDjr8X4fU6CRL1eWgdLwbWEpo++MPU/2P4PFk5ZLAdye9T5Sdp+MomePPJ/gHjLMj2VfQ==", "dependencies": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.10.3" + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-accounts": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.3.tgz", - "integrity": "sha512-8MipGgwusDVgn7NwKOmpeo3gxzzd+SmwcWeBdpXknuyDiZSQy9tXe+E9LeFGrmys/8mLLYP79n3jSbiTyv+6pQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.4.tgz", + "integrity": "sha512-ysy5sVTg9snYS7tJjxVoQAH6DTOTkRGR8emEVCWNGLGiB9txj+qDvSeT0izjurS/g7D5xlMAgrEHLK1Vi6I3yg==", "dependencies": { "@ethereumjs/common": "2.6.5", "@ethereumjs/tx": "3.5.2", @@ -16982,10 +17080,10 @@ "eth-lib": "0.2.8", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -17019,72 +17117,72 @@ } }, "node_modules/web3-eth-contract": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.3.tgz", - "integrity": "sha512-Y2CW61dCCyY4IoUMD4JsEQWrILX4FJWDWC/Txx/pr3K/+fGsBGvS9kWQN5EsVXOp4g7HoFOfVh9Lf7BmVVSRmg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.4.tgz", + "integrity": "sha512-Q8PfolOJ4eV9TvnTj1TGdZ4RarpSLmHnUnzVxZ/6/NiTfe4maJz99R0ISgwZkntLhLRtw0C7LRJuklzGYCNN3A==", "dependencies": { "@types/bn.js": "^5.1.1", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-contract/node_modules/@types/bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "dependencies": { "@types/node": "*" } }, "node_modules/web3-eth-ens": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.3.tgz", - "integrity": "sha512-hR+odRDXGqKemw1GFniKBEXpjYwLgttTES+bc7BfTeoUyUZXbyDHe5ifC+h+vpzxh4oS0TnfcIoarK0Z9tFSiQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.4.tgz", + "integrity": "sha512-LLrvxuFeVooRVZ9e5T6OWKVflHPFgrVjJ/jtisRWcmI7KN/b64+D/wJzXqgmp6CNsMQcE7rpmf4CQmJCrTdsgg==", "dependencies": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-eth-contract": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-eth-contract": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-iban": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.3.tgz", - "integrity": "sha512-ZCfOjYKAjaX2TGI8uif5ah+J3BYFuo+47JOIV1RIz2l7kD9VfnxvRH5UiQDRyMALQC7KFd2hUqIEtHklapNyKA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.4.tgz", + "integrity": "sha512-0gE5iNmOkmtBmbKH2aTodeompnNE8jEyvwFJ6s/AF6jkw9ky9Op9cqfzS56AYAbrqEFuClsqB/AoRves7LDELw==", "dependencies": { "bn.js": "^5.2.1", - "web3-utils": "1.10.3" + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-personal": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.3.tgz", - "integrity": "sha512-avrQ6yWdADIvuNQcFZXmGLCEzulQa76hUOuVywN7O3cklB4nFc/Gp3yTvD3bOAaE7DhjLQfhUTCzXL7WMxVTsw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.4.tgz", + "integrity": "sha512-BRa/hs6jU1hKHz+AC/YkM71RP3f0Yci1dPk4paOic53R4ZZG4MgwKRkJhgt3/GPuPliwS46f/i5A7fEGBT4F9w==", "dependencies": { "@types/node": "^12.12.6", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-net": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-net": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -17096,13 +17194,13 @@ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, "node_modules/web3-net": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.3.tgz", - "integrity": "sha512-IoSr33235qVoI1vtKssPUigJU9Fc/Ph0T9CgRi15sx+itysmvtlmXMNoyd6Xrgm9LuM4CIhxz7yDzH93B79IFg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.4.tgz", + "integrity": "sha512-mKINnhOOnZ4koA+yV2OT5s5ztVjIx7IY9a03w6s+yao/BUn+Luuty0/keNemZxTr1E8Ehvtn28vbOtW7Ids+Ow==", "dependencies": { - "web3-core": "1.10.3", - "web3-core-method": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-method": "1.10.4", + "web3-utils": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -17187,14 +17285,14 @@ } }, "node_modules/web3-providers-http": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.3.tgz", - "integrity": "sha512-6dAgsHR3MxJ0Qyu3QLFlQEelTapVfWNTu5F45FYh8t7Y03T1/o+YAkVxsbY5AdmD+y5bXG/XPJ4q8tjL6MgZHw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.4.tgz", + "integrity": "sha512-m2P5Idc8hdiO0l60O6DSCPw0kw64Zgi0pMjbEFRmxKIck2Py57RQMu4bxvkxJwkF06SlGaEQF8rFZBmuX7aagQ==", "dependencies": { "abortcontroller-polyfill": "^1.7.5", "cross-fetch": "^4.0.0", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "engines": { "node": ">=8.0.0" @@ -17209,24 +17307,24 @@ } }, "node_modules/web3-providers-ipc": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.3.tgz", - "integrity": "sha512-vP5WIGT8FLnGRfswTxNs9rMfS1vCbMezj/zHbBe/zB9GauBRTYVrUo2H/hVrhLg8Ut7AbsKZ+tCJ4mAwpKi2hA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.4.tgz", + "integrity": "sha512-YRF/bpQk9z3WwjT+A6FI/GmWRCASgd+gC0si7f9zbBWLXjwzYAKG73bQBaFRAHex1hl4CVcM5WUMaQXf3Opeuw==", "dependencies": { "oboe": "2.1.5", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ws": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.3.tgz", - "integrity": "sha512-/filBXRl48INxsh6AuCcsy4v5ndnTZ/p6bl67kmO9aK1wffv7CT++DrtclDtVMeDGCgB3van+hEf9xTAVXur7Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.4.tgz", + "integrity": "sha512-j3FBMifyuFFmUIPVQR4pj+t5ILhAexAui0opgcpu9R5LxQrLRUZxHSnU+YO25UycSOa/NAX8A+qkqZNpcFAlxA==", "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.10.3", + "web3-core-helpers": "1.10.4", "websocket": "^1.0.32" }, "engines": { @@ -17234,24 +17332,24 @@ } }, "node_modules/web3-shh": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.3.tgz", - "integrity": "sha512-cAZ60CPvs9azdwMSQ/PSUdyV4PEtaW5edAZhu3rCXf6XxQRliBboic+AvwUvB6j3eswY50VGa5FygfVmJ1JVng==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.4.tgz", + "integrity": "sha512-cOH6iFFM71lCNwSQrC3niqDXagMqrdfFW85hC9PFUrAr3PUrIem8TNstTc3xna2bwZeWG6OBy99xSIhBvyIACw==", "hasInstallScript": true, "dependencies": { - "web3-core": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-net": "1.10.3" + "web3-core": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-net": "1.10.4" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-utils": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.3.tgz", - "integrity": "sha512-OqcUrEE16fDBbGoQtZXWdavsPzbGIDc5v3VrRTZ0XrIpefC/viZ1ZU9bGEemazyS0catk/3rkOOxpzTfY+XsyQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", "dependencies": { "@ethereumjs/util": "^8.1.0", "bn.js": "^5.2.1", @@ -17267,14 +17365,14 @@ } }, "node_modules/web3-utils/node_modules/ethereum-cryptography": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", - "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz", + "integrity": "sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==", "dependencies": { - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" + "@noble/curves": "1.3.0", + "@noble/hashes": "1.3.3", + "@scure/bip32": "1.3.3", + "@scure/bip39": "1.2.2" } }, "node_modules/web3modal": { @@ -17300,19 +17398,19 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -17326,7 +17424,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -17735,9 +17833,9 @@ "dev": true }, "node_modules/xss": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", - "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" @@ -17838,15 +17936,15 @@ "dev": true }, "@amplitude/analytics-browser": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.3.3.tgz", - "integrity": "sha512-we1tw7fn+yyf2waAyw/XqBOTEwIEY19qXJ5B9d1Xc1SKNxb6HzU5IpD3zbTKwZLlCGKbkVY6wI3JxuuoYIAI2w==", - "requires": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", - "@amplitude/plugin-page-view-tracking-browser": "^2.0.13", - "@amplitude/plugin-web-attribution-browser": "^2.0.13", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.5.2.tgz", + "integrity": "sha512-SjYjOEXO2it0dylVHv3n812ReA62c7GmxoeZCMMg9IFzgM54J4XIUTXYTDhQI/uGawjWZcsiuSqFgS/7LcwuSg==", + "requires": { + "@amplitude/analytics-client-common": "^2.1.0", + "@amplitude/analytics-core": "^2.2.1", + "@amplitude/analytics-types": "^2.5.0", + "@amplitude/plugin-page-view-tracking-browser": "^2.2.2", + "@amplitude/plugin-web-attribution-browser": "^2.1.3", "tslib": "^2.4.1" }, "dependencies": { @@ -17858,13 +17956,13 @@ } }, "@amplitude/analytics-client-common": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.0.7.tgz", - "integrity": "sha512-2LqPMsY6ksS1OTda0EFPLUDFmIVTwU0bDN17+uY9WvpWTEWCAL616X2oNCQ6FTB9zWfCTr8X2I5jSS0y4rzPkw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.1.0.tgz", + "integrity": "sha512-wgZs7Orb3+ei7tBqxd5Ug3OzX19E0YpMkhHWFYesmpjBOW+Ng50rolyfB10rHjjeoMgiRc8eGDdnMH4+y7OQlA==", "requires": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-core": "^2.2.1", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" }, "dependencies": { @@ -17881,11 +17979,11 @@ "integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g==" }, "@amplitude/analytics-core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.1.0.tgz", - "integrity": "sha512-a7/WUacF+R6J8NTYf93gaVd3OjOyF0db2K9Y6+uSZp/xIbsGyR/47WN1R3CYPm4IC9Y9/ukBAHd/p4FsNJbsNg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.2.1.tgz", + "integrity": "sha512-F4IrNzealRXnC3cPjkOoOEd68xsxuDsTeEGErAdxqWXNqV1x9oy1WM1lIKw2yGOyf6OZHidP/Vbp/BJK6B0Fmg==", "requires": { - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" }, "dependencies": { @@ -17897,17 +17995,17 @@ } }, "@amplitude/analytics-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.3.0.tgz", - "integrity": "sha512-/sMCgimMzDjGZDh5ekHUrwKVi4IJc8/AFUXIxEuJn4wd9QVmThWNeKfszA36z6Ue+UOHhEUEZwruXo3PwXWz/g==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.5.0.tgz", + "integrity": "sha512-aY69WxUvVlaCU+9geShjTsAYdUTvegEXH9i4WK/97kNbNLl4/7qUuIPe4hNireDeKLuQA9SA3H7TKynuNomDxw==" }, "@amplitude/plugin-page-view-tracking-browser": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.0.13.tgz", - "integrity": "sha512-23X7tEublqLx6cs7o7KXyQ9UdfX0lSVNHDgurhuGdAV8yEeTdg9bRvVHGZETZtnbQj31avD7M2U31nXXaopX3A==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.2.2.tgz", + "integrity": "sha512-onsguK2fugi58vhW/NvuAqiQ7TG7IGmUKTg9Zfn39Y2IjudgT8N68LXz6esIY0XKQhZFHVsBT3pqYgEVK5I6Tg==", "requires": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-client-common": "^2.1.0", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" }, "dependencies": { @@ -17919,13 +18017,13 @@ } }, "@amplitude/plugin-web-attribution-browser": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.0.13.tgz", - "integrity": "sha512-oVsVxNk/twdcE5r5v+ALX3C443Q1NQGTaKNvbxyO/k3DiEGmKpgC8hSj9S5O7ZdrII63cS+u0IHevRXgmEnplg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-2.1.3.tgz", + "integrity": "sha512-CLTzgtewfgSLkwACkvPJ679Yr8HRiy1p8u9Nt5rvKlz3UeHXP7PqTQ/PC/nPp4AuaX30HB3xu4Eof8onWNm4nA==", "requires": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", + "@amplitude/analytics-client-common": "^2.1.0", + "@amplitude/analytics-core": "^2.2.1", + "@amplitude/analytics-types": "^2.5.0", "tslib": "^2.4.1" }, "dependencies": { @@ -17957,34 +18055,34 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "requires": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" } }, "@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==" + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==" }, "@babel/core": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", - "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -18000,11 +18098,11 @@ } }, "@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "requires": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -18019,22 +18117,22 @@ } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", - "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" } }, "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -18055,15 +18153,15 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz", - "integrity": "sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -18072,14 +18170,14 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", - "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", "regexpu-core": "^5.3.1", - "semver": "^6.3.0" + "semver": "^6.3.1" } }, "@babel/helper-define-polyfill-provider": { @@ -18118,12 +18216,12 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.23.0" } }, "@babel/helper-module-imports": { @@ -18135,9 +18233,9 @@ } }, "@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "requires": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -18156,9 +18254,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==" }, "@babel/helper-remap-async-to-generator": { "version": "7.22.20", @@ -18172,13 +18270,13 @@ } }, "@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" } }, @@ -18208,9 +18306,9 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" }, "@babel/helper-validator-identifier": { "version": "7.22.20", @@ -18218,9 +18316,9 @@ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==" + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==" }, "@babel/helper-wrap-function": { "version": "7.22.20", @@ -18234,48 +18332,58 @@ } }, "@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" } }, "@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "requires": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/plugin-transform-optional-chaining": "^7.23.3" + } + }, + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-proposal-private-property-in-object": { @@ -18340,18 +18448,18 @@ } }, "@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" @@ -18476,18 +18584,18 @@ } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", - "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.22.20", @@ -18497,114 +18605,113 @@ } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" } }, "@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" } }, "@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" } }, "@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18612,19 +18719,19 @@ } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18632,29 +18739,30 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" } }, "@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18662,18 +18770,18 @@ } }, "@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18681,54 +18789,54 @@ } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", + "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" } }, @@ -18743,18 +18851,18 @@ } }, "@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18762,9 +18870,9 @@ } }, "@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18772,32 +18880,32 @@ } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.0.tgz", + "integrity": "sha512-y/yKMm7buHpFFXfxVFS4Vk1ToRJDilIa6fKRioB9Vjichv58TDGXTvqV0dN7plobAmTW5eSEGXDngE+Mm+uO+w==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/plugin-transform-parameters": "^7.23.3" } }, "@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" } }, "@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18805,9 +18913,9 @@ } }, "@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18816,49 +18924,49 @@ } }, "@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" } }, "@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18866,9 +18974,9 @@ } }, "@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" @@ -18899,18 +19007,18 @@ } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5", @@ -18918,91 +19026,92 @@ } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" } }, "@babel/preset-env": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", - "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.23.2", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -19014,67 +19123,66 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.23.2", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.23.0", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.23.0", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-modules-systemjs": "^7.23.0", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.23.0", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, "dependencies": { "@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "requires": { "@babel/helper-compilation-targets": "^7.22.6", @@ -19085,23 +19193,23 @@ } }, "babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "requires": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.5.0" } } } @@ -19132,38 +19240,38 @@ } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" } }, "@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "requires": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } @@ -19219,9 +19327,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -19242,9 +19350,9 @@ "dev": true }, "globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -19268,9 +19376,9 @@ } }, "@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true }, "@ethereumjs/common": { @@ -19307,14 +19415,14 @@ }, "dependencies": { "ethereum-cryptography": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", - "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz", + "integrity": "sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==", "requires": { - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" + "@noble/curves": "1.3.0", + "@noble/hashes": "1.3.3", + "@scure/bip32": "1.3.3", + "@scure/bip39": "1.2.2" } } } @@ -19514,18 +19622,18 @@ } }, "@fortawesome/fontawesome-free": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", - "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==" + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", + "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==" }, "@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" } }, @@ -19536,9 +19644,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -20093,9 +20201,9 @@ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", @@ -20108,12 +20216,12 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@kurkle/color": { @@ -20127,17 +20235,17 @@ "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==" }, "@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "requires": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.3.3" } }, "@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -20166,27 +20274,27 @@ } }, "@scure/base": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", - "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" }, "@scure/bip32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", + "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", "requires": { - "@noble/curves": "~1.1.0", - "@noble/hashes": "~1.3.1", - "@scure/base": "~1.1.0" + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" } }, "@scure/bip39": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.2.tgz", + "integrity": "sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==", "requires": { - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" } }, "@sinclair/typebox": { @@ -20200,6 +20308,12 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" }, + "@sindresorhus/merge-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", + "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "dev": true + }, "@sinonjs/commons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", @@ -20318,9 +20432,9 @@ } }, "@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "@types/graceful-fs": { @@ -20333,9 +20447,9 @@ } }, "@types/http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" }, "@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -20406,9 +20520,9 @@ } }, "@types/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "requires": { "@types/node": "*" } @@ -21207,14 +21321,14 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "dev": true, "requires": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -21362,19 +21476,19 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.5.tgz", - "integrity": "sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.32.2" + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" }, "dependencies": { "@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "requires": { "@babel/helper-compilation-targets": "^7.22.6", @@ -21658,13 +21772,13 @@ } }, "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" } }, @@ -21856,9 +21970,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001549", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz", - "integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==" + "version": "1.0.30001593", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz", + "integrity": "sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==" }, "caseless": { "version": "0.12.0", @@ -21895,9 +22009,9 @@ "dev": true }, "chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", "requires": { "@kurkle/color": "^0.3.0" } @@ -22179,30 +22293,30 @@ } }, "copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "requires": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" } }, "core-js": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.2.tgz", - "integrity": "sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==" + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==" }, "core-js-compat": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", - "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", "requires": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" } }, "core-util-is": { @@ -22220,15 +22334,15 @@ } }, "cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "requires": { - "import-fresh": "^3.2.1", + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "dependencies": { "argparse": { @@ -22404,26 +22518,26 @@ "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" }, "css-declaration-sorter": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", - "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.1.1.tgz", + "integrity": "sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==", "dev": true, "requires": {} }, "css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "requires": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "dependencies": { "semver": { @@ -22438,17 +22552,17 @@ } }, "css-minimizer-webpack-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", - "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-6.0.0.tgz", + "integrity": "sha512-BLpR9CCDkKvhO3i0oZQgad6v9pCxUuhSc5RT6iUEy9M8hBXi4TJb5vqF2GQ2deqYHmRi3O6IR9hgAZQWg0EBwA==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.18", - "cssnano": "^6.0.1", - "jest-worker": "^29.4.3", - "postcss": "^8.4.24", - "schema-utils": "^4.0.1", - "serialize-javascript": "^6.0.1" + "@jridgewell/trace-mapping": "^0.3.21", + "cssnano": "^6.0.3", + "jest-worker": "^29.7.0", + "postcss": "^8.4.33", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "dependencies": { "has-flag": { @@ -22458,13 +22572,13 @@ "dev": true }, "jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } @@ -22531,56 +22645,56 @@ "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" }, "cssnano": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.1.tgz", - "integrity": "sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz", + "integrity": "sha512-MRq4CIj8pnyZpcI2qs6wswoYoDD1t0aL28n+41c1Ukcpm56m1h6mCexIHBGjfZfnTqtGSSCP4/fB1ovxgjBOiw==", "dev": true, "requires": { - "cssnano-preset-default": "^6.0.1", - "lilconfig": "^2.1.0" + "cssnano-preset-default": "^6.0.3", + "lilconfig": "^3.0.0" } }, "cssnano-preset-default": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.1.tgz", - "integrity": "sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ==", - "dev": true, - "requires": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^4.0.0", - "postcss-calc": "^9.0.0", - "postcss-colormin": "^6.0.0", - "postcss-convert-values": "^6.0.0", - "postcss-discard-comments": "^6.0.0", - "postcss-discard-duplicates": "^6.0.0", - "postcss-discard-empty": "^6.0.0", - "postcss-discard-overridden": "^6.0.0", - "postcss-merge-longhand": "^6.0.0", - "postcss-merge-rules": "^6.0.1", - "postcss-minify-font-values": "^6.0.0", - "postcss-minify-gradients": "^6.0.0", - "postcss-minify-params": "^6.0.0", - "postcss-minify-selectors": "^6.0.0", - "postcss-normalize-charset": "^6.0.0", - "postcss-normalize-display-values": "^6.0.0", - "postcss-normalize-positions": "^6.0.0", - "postcss-normalize-repeat-style": "^6.0.0", - "postcss-normalize-string": "^6.0.0", - "postcss-normalize-timing-functions": "^6.0.0", - "postcss-normalize-unicode": "^6.0.0", - "postcss-normalize-url": "^6.0.0", - "postcss-normalize-whitespace": "^6.0.0", - "postcss-ordered-values": "^6.0.0", - "postcss-reduce-initial": "^6.0.0", - "postcss-reduce-transforms": "^6.0.0", - "postcss-svgo": "^6.0.0", - "postcss-unique-selectors": "^6.0.0" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.3.tgz", + "integrity": "sha512-4y3H370aZCkT9Ev8P4SO4bZbt+AExeKhh8wTbms/X7OLDo5E7AYUUy6YPxa/uF5Grf+AJwNcCnxKhZynJ6luBA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^7.1.1", + "cssnano-utils": "^4.0.1", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.0.2", + "postcss-convert-values": "^6.0.2", + "postcss-discard-comments": "^6.0.1", + "postcss-discard-duplicates": "^6.0.1", + "postcss-discard-empty": "^6.0.1", + "postcss-discard-overridden": "^6.0.1", + "postcss-merge-longhand": "^6.0.2", + "postcss-merge-rules": "^6.0.3", + "postcss-minify-font-values": "^6.0.1", + "postcss-minify-gradients": "^6.0.1", + "postcss-minify-params": "^6.0.2", + "postcss-minify-selectors": "^6.0.2", + "postcss-normalize-charset": "^6.0.1", + "postcss-normalize-display-values": "^6.0.1", + "postcss-normalize-positions": "^6.0.1", + "postcss-normalize-repeat-style": "^6.0.1", + "postcss-normalize-string": "^6.0.1", + "postcss-normalize-timing-functions": "^6.0.1", + "postcss-normalize-unicode": "^6.0.2", + "postcss-normalize-url": "^6.0.1", + "postcss-normalize-whitespace": "^6.0.1", + "postcss-ordered-values": "^6.0.1", + "postcss-reduce-initial": "^6.0.2", + "postcss-reduce-transforms": "^6.0.1", + "postcss-svgo": "^6.0.2", + "postcss-unique-selectors": "^6.0.2" } }, "cssnano-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.0.tgz", - "integrity": "sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.1.tgz", + "integrity": "sha512-6qQuYDqsGoiXssZ3zct6dcMxiqfT6epy7x4R0TQJadd4LWO3sPR6JH6ZByOvVLoZ6EdwPGgd7+DR1EmX3tiXQQ==", "dev": true, "requires": {} }, @@ -22828,15 +22942,6 @@ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.2.tgz", "integrity": "sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==" }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -22917,9 +23022,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.555", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.555.tgz", - "integrity": "sha512-k1wGC7UXDTyCWcONkEMRG/w6Jvrxi+SVEU+IeqUKUKjv2lGJ1b+jf1mqrloyxVTG5WYYjNQ+F6+Cb1fGrLvNcA==" + "version": "1.4.692", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.692.tgz", + "integrity": "sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==" }, "elliptic": { "version": "6.5.4", @@ -23011,6 +23116,12 @@ "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", "dev": true }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, "envinfo": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", @@ -23119,12 +23230,13 @@ } }, "es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "requires": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, @@ -23228,16 +23340,16 @@ } }, "eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -23471,9 +23583,9 @@ } }, "eslint-plugin-import": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", - "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "requires": { "array-includes": "^3.1.7", @@ -23492,7 +23604,7 @@ "object.groupby": "^1.0.1", "object.values": "^1.1.7", "semver": "^6.3.1", - "tsconfig-paths": "^3.14.2" + "tsconfig-paths": "^3.15.0" }, "dependencies": { "debug": { @@ -23627,6 +23739,24 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, + "esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + } + } + }, "espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -24080,9 +24210,9 @@ }, "dependencies": { "@types/bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "requires": { "@types/node": "*" } @@ -24190,6 +24320,15 @@ "strip-hex-prefix": "1.0.0" } }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "eventemitter3": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", @@ -24380,9 +24519,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -24591,9 +24730,9 @@ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, "fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true }, "fresh": { @@ -24768,22 +24907,29 @@ } }, "globby": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.2.tgz", - "integrity": "sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", + "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", "dev": true, "requires": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "@sindresorhus/merge-streams": "^1.0.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "dependencies": { + "path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true + }, "slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true } } @@ -24991,9 +25137,9 @@ } }, "http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -25061,9 +25207,9 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true }, "immediate": { @@ -26725,9 +26871,9 @@ } }, "jiti": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", - "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true }, "jquery": { @@ -27098,9 +27244,9 @@ } }, "lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", "dev": true }, "lines-and-columns": { @@ -27270,9 +27416,9 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, "luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==" + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" }, "make-dir": { "version": "4.0.0", @@ -27509,12 +27655,13 @@ } }, "mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", "dev": true, "requires": { - "schema-utils": "^4.0.0" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" } }, "minimalistic-assert": { @@ -27566,9 +27713,9 @@ } }, "mixpanel-browser": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", - "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.49.0.tgz", + "integrity": "sha512-RZJCO7XXuuHBAWG5fd9Mavz994M7v7W3Qiaq8NzmN631pa4BQ0vNZQtRFqKcCCOBn4xqOZbX2GkuC7ZkQoL4cQ==" }, "mkdirp": { "version": "3.0.1", @@ -27589,9 +27736,9 @@ "integrity": "sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw==" }, "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" }, "ms": { "version": "2.1.2", @@ -27647,9 +27794,9 @@ "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, "nanomorph": { @@ -27728,9 +27875,9 @@ "dev": true }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "normalize-path": { "version": "3.0.0", @@ -28047,12 +28194,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, "pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -28082,9 +28223,9 @@ "version": "file:../../../deps/phoenix_html" }, "photoswipe": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.2.tgz", - "integrity": "sha512-z5hr36nAIPOZbHJPbCJ/mQ3+ZlizttF9za5gKXKH/us1k4KNHaRbC63K1Px5sVVKUtGb/2+ixHpKqtwl0WAwvA==" + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.3.tgz", + "integrity": "sha512-9UC6oJBK4oXFZ5HcdlcvGkfEHsVrmE4csUdCQhEjHYb3PvPLO3PG7UhnPuOgjxwmhq5s17Un5NUdum01LgBDng==" }, "picocolors": { "version": "1.0.0", @@ -28184,12 +28325,12 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -28205,70 +28346,70 @@ } }, "postcss-colormin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.0.tgz", - "integrity": "sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.2.tgz", + "integrity": "sha512-TXKOxs9LWcdYo5cgmcSHPkyrLAh86hX1ijmyy6J8SbOhyv6ua053M3ZAM/0j44UsnQNIWdl8gb5L7xX2htKeLw==", "dev": true, "requires": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0", "colord": "^2.9.1", "postcss-value-parser": "^4.2.0" } }, "postcss-convert-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.0.tgz", - "integrity": "sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.2.tgz", + "integrity": "sha512-aeBmaTnGQ+NUSVQT8aY0sKyAD/BaLJenEKZ03YK0JnDE1w1Rr8XShoxdal2V2H26xTJKr3v5haByOhJuyT4UYw==", "dev": true, "requires": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "postcss-value-parser": "^4.2.0" } }, "postcss-discard-comments": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.0.tgz", - "integrity": "sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.1.tgz", + "integrity": "sha512-f1KYNPtqYLUeZGCHQPKzzFtsHaRuECe6jLakf/RjSRqvF5XHLZnM2+fXLhb8Qh/HBFHs3M4cSLb1k3B899RYIg==", "dev": true, "requires": {} }, "postcss-discard-duplicates": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.0.tgz", - "integrity": "sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.1.tgz", + "integrity": "sha512-1hvUs76HLYR8zkScbwyJ8oJEugfPV+WchpnA+26fpJ7Smzs51CzGBHC32RS03psuX/2l0l0UKh2StzNxOrKCYg==", "dev": true, "requires": {} }, "postcss-discard-empty": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.0.tgz", - "integrity": "sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.1.tgz", + "integrity": "sha512-yitcmKwmVWtNsrrRqGJ7/C0YRy53i0mjexBDQ9zYxDwTWVBgbU4+C9jIZLmQlTDT9zhml+u0OMFJh8+31krmOg==", "dev": true, "requires": {} }, "postcss-discard-overridden": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz", - "integrity": "sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.1.tgz", + "integrity": "sha512-qs0ehZMMZpSESbRkw1+inkf51kak6OOzNRaoLd/U7Fatp0aN2HQ1rxGOrJvYcRAN9VpX8kUF13R2ofn8OlvFVA==", "dev": true, "requires": {} }, "postcss-loader": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", - "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, "requires": { - "cosmiconfig": "^8.2.0", - "jiti": "^1.18.2", - "semver": "^7.3.8" + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -28277,65 +28418,65 @@ } }, "postcss-merge-longhand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz", - "integrity": "sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.2.tgz", + "integrity": "sha512-+yfVB7gEM8SrCo9w2lCApKIEzrTKl5yS1F4yGhV3kSim6JzbfLGJyhR1B6X+6vOT0U33Mgx7iv4X9MVWuaSAfw==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.0.0" + "stylehacks": "^6.0.2" } }, "postcss-merge-rules": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.1.tgz", - "integrity": "sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.3.tgz", + "integrity": "sha512-yfkDqSHGohy8sGYIJwBmIGDv4K4/WrJPX355XrxQb/CSsT4Kc/RxDi6akqn5s9bap85AWgv21ArcUWwWdGNSHA==", "dev": true, "requires": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.0", - "postcss-selector-parser": "^6.0.5" + "cssnano-utils": "^4.0.1", + "postcss-selector-parser": "^6.0.15" } }, "postcss-minify-font-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.0.tgz", - "integrity": "sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.1.tgz", + "integrity": "sha512-tIwmF1zUPoN6xOtA/2FgVk1ZKrLcCvE0dpZLtzyyte0j9zUeB8RTbCqrHZGjJlxOvNWKMYtunLrrl7HPOiR46w==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-minify-gradients": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.0.tgz", - "integrity": "sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.1.tgz", + "integrity": "sha512-M1RJWVjd6IOLPl1hYiOd5HQHgpp6cvJVLrieQYS9y07Yo8itAr6jaekzJphaJFR0tcg4kRewCk3kna9uHBxn/w==", "dev": true, "requires": { "colord": "^2.9.1", - "cssnano-utils": "^4.0.0", + "cssnano-utils": "^4.0.1", "postcss-value-parser": "^4.2.0" } }, "postcss-minify-params": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.0.tgz", - "integrity": "sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.2.tgz", + "integrity": "sha512-zwQtbrPEBDj+ApELZ6QylLf2/c5zmASoOuA4DzolyVGdV38iR2I5QRMsZcHkcdkZzxpN8RS4cN7LPskOkTwTZw==", "dev": true, "requires": { - "browserslist": "^4.21.4", - "cssnano-utils": "^4.0.0", + "browserslist": "^4.22.2", + "cssnano-utils": "^4.0.1", "postcss-value-parser": "^4.2.0" } }, "postcss-minify-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.0.tgz", - "integrity": "sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.2.tgz", + "integrity": "sha512-0b+m+w7OAvZejPQdN2GjsXLv5o0jqYHX3aoV0e7RBKPCsB7TYG5KKWBFhGnB/iP3213Ts8c5H4wLPLMm7z28Sg==", "dev": true, "requires": { - "postcss-selector-parser": "^6.0.5" + "postcss-selector-parser": "^6.0.15" } }, "postcss-modules-extract-imports": { @@ -28346,9 +28487,9 @@ "requires": {} }, "postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, "requires": { "icss-utils": "^5.0.0", @@ -28357,9 +28498,9 @@ } }, "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, "requires": { "postcss-selector-parser": "^6.0.4" @@ -28375,118 +28516,118 @@ } }, "postcss-normalize-charset": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.0.tgz", - "integrity": "sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.1.tgz", + "integrity": "sha512-aW5LbMNRZ+oDV57PF9K+WI1Z8MPnF+A8qbajg/T8PP126YrGX1f9IQx21GI2OlGz7XFJi/fNi0GTbY948XJtXg==", "dev": true, "requires": {} }, "postcss-normalize-display-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.0.tgz", - "integrity": "sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.1.tgz", + "integrity": "sha512-mc3vxp2bEuCb4LgCcmG1y6lKJu1Co8T+rKHrcbShJwUmKJiEl761qb/QQCfFwlrvSeET3jksolCR/RZuMURudw==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-positions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.0.tgz", - "integrity": "sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.1.tgz", + "integrity": "sha512-HRsq8u/0unKNvm0cvwxcOUEcakFXqZ41fv3FOdPn916XFUrympjr+03oaLkuZENz3HE9RrQE9yU0Xv43ThWjQg==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-repeat-style": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.0.tgz", - "integrity": "sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.1.tgz", + "integrity": "sha512-Gbb2nmCy6tTiA7Sh2MBs3fj9W8swonk6lw+dFFeQT68B0Pzwp1kvisJQkdV6rbbMSd9brMlS8I8ts52tAGWmGQ==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-string": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.0.tgz", - "integrity": "sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.1.tgz", + "integrity": "sha512-5Fhx/+xzALJD9EI26Aq23hXwmv97Zfy2VFrt5PLT8lAhnBIZvmaT5pQk+NuJ/GWj/QWaKSKbnoKDGLbV6qnhXg==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-timing-functions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.0.tgz", - "integrity": "sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.1.tgz", + "integrity": "sha512-4zcczzHqmCU7L5dqTB9rzeqPWRMc0K2HoR+Bfl+FSMbqGBUcP5LRfgcH4BdRtLuzVQK1/FHdFoGT3F7rkEnY+g==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-unicode": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.0.tgz", - "integrity": "sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.2.tgz", + "integrity": "sha512-Ff2VdAYCTGyMUwpevTZPZ4w0+mPjbZzLLyoLh/RMpqUqeQKZ+xMm31hkxBavDcGKcxm6ACzGk0nBfZ8LZkStKA==", "dev": true, "requires": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.0.tgz", - "integrity": "sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.1.tgz", + "integrity": "sha512-jEXL15tXSvbjm0yzUV7FBiEXwhIa9H88JOXDGQzmcWoB4mSjZIsmtto066s2iW9FYuIrIF4k04HA2BKAOpbsaQ==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-normalize-whitespace": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.0.tgz", - "integrity": "sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.1.tgz", + "integrity": "sha512-76i3NpWf6bB8UHlVuLRxG4zW2YykF9CTEcq/9LGAiz2qBuX5cBStadkk0jSkg9a9TCIXbMQz7yzrygKoCW9JuA==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-ordered-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.0.tgz", - "integrity": "sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.1.tgz", + "integrity": "sha512-XXbb1O/MW9HdEhnBxitZpPFbIvDgbo9NK4c/5bOfiKpnIGZDoL2xd7/e6jW5DYLsWxBbs+1nZEnVgnjnlFViaA==", "dev": true, "requires": { - "cssnano-utils": "^4.0.0", + "cssnano-utils": "^4.0.1", "postcss-value-parser": "^4.2.0" } }, "postcss-reduce-initial": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", - "integrity": "sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.2.tgz", + "integrity": "sha512-YGKalhNlCLcjcLvjU5nF8FyeCTkCO5UtvJEt0hrPZVCTtRLSOH4z00T1UntQPj4dUmIYZgMj8qK77JbSX95hSw==", "dev": true, "requires": { - "browserslist": "^4.21.4", + "browserslist": "^4.22.2", "caniuse-api": "^3.0.0" } }, "postcss-reduce-transforms": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.0.tgz", - "integrity": "sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.1.tgz", + "integrity": "sha512-fUbV81OkUe75JM+VYO1gr/IoA2b/dRiH6HvMwhrIBSUrxq3jNZQZitSnugcTLDi1KkQh1eR/zi+iyxviUNBkcQ==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0" } }, "postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, "requires": { "cssesc": "^3.0.0", @@ -28494,22 +28635,22 @@ } }, "postcss-svgo": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.0.tgz", - "integrity": "sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.2.tgz", + "integrity": "sha512-IH5R9SjkTkh0kfFOQDImyy1+mTCb+E830+9SV1O+AaDcoHTvfsvt6WwJeo7KwcHbFnevZVCsXhDmjFiGVuwqFQ==", "dev": true, "requires": { "postcss-value-parser": "^4.2.0", - "svgo": "^3.0.2" + "svgo": "^3.2.0" } }, "postcss-unique-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.0.tgz", - "integrity": "sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.2.tgz", + "integrity": "sha512-8IZGQ94nechdG7Y9Sh9FlIY2b4uS8/k8kdKRX040XHsS3B6d1HrJAkXrBSsSu4SuARruSsUjW3nlSw8BHkaAYQ==", "dev": true, "requires": { - "postcss-selector-parser": "^6.0.5" + "postcss-selector-parser": "^6.0.15" } }, "postcss-value-parser": { @@ -28914,12 +29055,9 @@ "integrity": "sha512-Mb2WZ2bJF597exiqX7owBzrqJ74DHLK3yOQjCyPAaNifRncE8OD0wFIuoMhXxTnHK07+8zZ2SJEKy/qtiyR7vw==" }, "redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "requires": { - "@babel/runtime": "^7.9.2" - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "regenerate": { "version": "1.4.2", @@ -29213,9 +29351,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -29224,9 +29362,9 @@ } }, "sass-loader": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", - "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", "dev": true, "requires": { "neo-async": "^2.6.2" @@ -29251,9 +29389,9 @@ } }, "schema-utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.1.0.tgz", - "integrity": "sha512-Jw+GZVbP5IggB2WAn6UHI02LBwGmsIeYN/lNbSMZyDziQ7jmtAUrqKqDja+W89YHVs+KL/3IkIMltAklqB1vAw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", @@ -29369,9 +29507,9 @@ } }, "serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -29734,9 +29872,9 @@ "dev": true }, "style-loader": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", - "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", "dev": true, "requires": {} }, @@ -29758,13 +29896,13 @@ } }, "stylehacks": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.0.tgz", - "integrity": "sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.2.tgz", + "integrity": "sha512-00zvJGnCu64EpMjX8b5iCZ3us2Ptyw8+toEkb92VdmkEaRaSGBNKAoK6aWZckhXxmQP8zWiTaFaiMGIU8Ve8sg==", "dev": true, "requires": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" + "browserslist": "^4.22.2", + "postcss-selector-parser": "^6.0.15" } }, "supports-color": { @@ -29781,15 +29919,16 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "svgo": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz", - "integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", "dev": true, "requires": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", - "css-tree": "^2.2.1", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" } @@ -29865,9 +30004,9 @@ } }, "sweetalert2": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.9.0.tgz", - "integrity": "sha512-PA3qinKZMNGAhA+AUu2wU7yQOpeZCgOaYWRcg26f4cZN6f7M9iPBuobsxOhR9EHs7ihUIxT6vhAMiB4kcmk1SA==" + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.10.5.tgz", + "integrity": "sha512-q9eE3EKhMcpIDU/Xcz7z5lk8axCGkgxwK47gXGrrfncnBJWxHPPHnBVAjfsVXcTt8Yi8U6HNEcBRSu+qGeyFdA==" }, "symbol-tree": { "version": "3.2.4", @@ -29916,13 +30055,13 @@ } }, "terser": { - "version": "5.16.9", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.9.tgz", - "integrity": "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -29936,22 +30075,22 @@ } }, "terser-webpack-plugin": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", - "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.5" + "terser": "^5.26.0" }, "dependencies": { "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { "@types/json-schema": "^7.0.8", @@ -30048,9 +30187,9 @@ } }, "tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "requires": { "@types/json5": "^0.0.29", @@ -30228,6 +30367,12 @@ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true }, + "unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -30413,23 +30558,23 @@ } }, "web3": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.3.tgz", - "integrity": "sha512-DgUdOOqC/gTqW+VQl1EdPxrVRPB66xVNtuZ5KD4adVBtko87hkgM8BTZ0lZ8IbUfnQk6DyjcDujMiH3oszllAw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.4.tgz", + "integrity": "sha512-kgJvQZjkmjOEKimx/tJQsqWfRDPTTcBfYPa9XletxuHLpHcXdx67w8EFn5AW3eVxCutE9dTVHgGa9VYe8vgsEA==", "requires": { - "web3-bzz": "1.10.3", - "web3-core": "1.10.3", - "web3-eth": "1.10.3", - "web3-eth-personal": "1.10.3", - "web3-net": "1.10.3", - "web3-shh": "1.10.3", - "web3-utils": "1.10.3" + "web3-bzz": "1.10.4", + "web3-core": "1.10.4", + "web3-eth": "1.10.4", + "web3-eth-personal": "1.10.4", + "web3-net": "1.10.4", + "web3-shh": "1.10.4", + "web3-utils": "1.10.4" } }, "web3-bzz": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.3.tgz", - "integrity": "sha512-XDIRsTwekdBXtFytMpHBuun4cK4x0ZMIDXSoo1UVYp+oMyZj07c7gf7tNQY5qZ/sN+CJIas4ilhN25VJcjSijQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.4.tgz", + "integrity": "sha512-ZZ/X4sJ0Uh2teU9lAGNS8EjveEppoHNQiKlOXAjedsrdWuaMErBPdLQjXfcrYvN6WM6Su9PMsAxf3FXXZ+HwQw==", "requires": { "@types/node": "^12.12.6", "got": "12.1.0", @@ -30444,23 +30589,23 @@ } }, "web3-core": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.3.tgz", - "integrity": "sha512-Vbk0/vUNZxJlz3RFjAhNNt7qTpX8yE3dn3uFxfX5OHbuon5u65YEOd3civ/aQNW745N0vGUlHFNxxmn+sG9DIw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.4.tgz", + "integrity": "sha512-B6elffYm81MYZDTrat7aEhnhdtVE3lDBUZft16Z8awYMZYJDbnykEbJVS+l3mnA7AQTnSDr/1MjWofGDLBJPww==", "requires": { "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-requestmanager": "1.10.3", - "web3-utils": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-requestmanager": "1.10.4", + "web3-utils": "1.10.4" }, "dependencies": { "@types/bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "requires": { "@types/node": "*" } @@ -30473,87 +30618,87 @@ } }, "web3-core-helpers": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.3.tgz", - "integrity": "sha512-Yv7dQC3B9ipOc5sWm3VAz1ys70Izfzb8n9rSiQYIPjpqtJM+3V4EeK6ghzNR6CO2es0+Yu9CtCkw0h8gQhrTxA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.4.tgz", + "integrity": "sha512-r+L5ylA17JlD1vwS8rjhWr0qg7zVoVMDvWhajWA5r5+USdh91jRUYosp19Kd1m2vE034v7Dfqe1xYRoH2zvG0g==", "requires": { - "web3-eth-iban": "1.10.3", - "web3-utils": "1.10.3" + "web3-eth-iban": "1.10.4", + "web3-utils": "1.10.4" } }, "web3-core-method": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.3.tgz", - "integrity": "sha512-VZ/Dmml4NBmb0ep5PTSg9oqKoBtG0/YoMPei/bq/tUdlhB2dMB79sbeJPwx592uaV0Vpk7VltrrrBv5hTM1y4Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.4.tgz", + "integrity": "sha512-uZTb7flr+Xl6LaDsyTeE2L1TylokCJwTDrIVfIfnrGmnwLc6bmTWCCrm71sSrQ0hqs6vp/MKbQYIYqUN0J8WyA==", "requires": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-utils": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-utils": "1.10.4" } }, "web3-core-promievent": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.3.tgz", - "integrity": "sha512-HgjY+TkuLm5uTwUtaAfkTgRx/NzMxvVradCi02gy17NxDVdg/p6svBHcp037vcNpkuGeFznFJgULP+s2hdVgUQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.4.tgz", + "integrity": "sha512-2de5WnJQ72YcIhYwV/jHLc4/cWJnznuoGTJGD29ncFQHAfwW/MItHFSVKPPA5v8AhJe+r6y4Y12EKvZKjQVBvQ==", "requires": { "eventemitter3": "4.0.4" } }, "web3-core-requestmanager": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.3.tgz", - "integrity": "sha512-VT9sKJfgM2yBOIxOXeXiDuFMP4pxzF6FT+y8KTLqhDFHkbG3XRe42Vm97mB/IvLQCJOmokEjl3ps8yP1kbggyw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.4.tgz", + "integrity": "sha512-vqP6pKH8RrhT/2MoaU+DY/OsYK9h7HmEBNCdoMj+4ZwujQtw/Mq2JifjwsJ7gits7Q+HWJwx8q6WmQoVZAWugg==", "requires": { "util": "^0.12.5", - "web3-core-helpers": "1.10.3", - "web3-providers-http": "1.10.3", - "web3-providers-ipc": "1.10.3", - "web3-providers-ws": "1.10.3" + "web3-core-helpers": "1.10.4", + "web3-providers-http": "1.10.4", + "web3-providers-ipc": "1.10.4", + "web3-providers-ws": "1.10.4" } }, "web3-core-subscriptions": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.3.tgz", - "integrity": "sha512-KW0Mc8sgn70WadZu7RjQ4H5sNDJ5Lx8JMI3BWos+f2rW0foegOCyWhRu33W1s6ntXnqeBUw5rRCXZRlA3z+HNA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.4.tgz", + "integrity": "sha512-o0lSQo/N/f7/L76C0HV63+S54loXiE9fUPfHFcTtpJRQNDBVsSDdWRdePbWwR206XlsBqD5VHApck1//jEafTw==", "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" } }, "web3-eth": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.3.tgz", - "integrity": "sha512-Uk1U2qGiif2mIG8iKu23/EQJ2ksB1BQXy3wF3RvFuyxt8Ft9OEpmGlO7wOtAyJdoKzD5vcul19bJpPcWSAYZhA==", - "requires": { - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-eth-accounts": "1.10.3", - "web3-eth-contract": "1.10.3", - "web3-eth-ens": "1.10.3", - "web3-eth-iban": "1.10.3", - "web3-eth-personal": "1.10.3", - "web3-net": "1.10.3", - "web3-utils": "1.10.3" + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.4.tgz", + "integrity": "sha512-Sql2kYKmgt+T/cgvg7b9ce24uLS7xbFrxE4kuuor1zSCGrjhTJ5rRNG8gTJUkAJGKJc7KgnWmgW+cOfMBPUDSA==", + "requires": { + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-eth-accounts": "1.10.4", + "web3-eth-contract": "1.10.4", + "web3-eth-ens": "1.10.4", + "web3-eth-iban": "1.10.4", + "web3-eth-personal": "1.10.4", + "web3-net": "1.10.4", + "web3-utils": "1.10.4" } }, "web3-eth-abi": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.3.tgz", - "integrity": "sha512-O8EvV67uhq0OiCMekqYsDtb6FzfYzMXT7VMHowF8HV6qLZXCGTdB/NH4nJrEh2mFtEwVdS6AmLFJAQd2kVyoMQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.4.tgz", + "integrity": "sha512-cZ0q65eJIkd/jyOlQPDjr8X4fU6CRL1eWgdLwbWEpo++MPU/2P4PFk5ZLAdye9T5Sdp+MomePPJ/gHjLMj2VfQ==", "requires": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.10.3" + "web3-utils": "1.10.4" } }, "web3-eth-accounts": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.3.tgz", - "integrity": "sha512-8MipGgwusDVgn7NwKOmpeo3gxzzd+SmwcWeBdpXknuyDiZSQy9tXe+E9LeFGrmys/8mLLYP79n3jSbiTyv+6pQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.4.tgz", + "integrity": "sha512-ysy5sVTg9snYS7tJjxVoQAH6DTOTkRGR8emEVCWNGLGiB9txj+qDvSeT0izjurS/g7D5xlMAgrEHLK1Vi6I3yg==", "requires": { "@ethereumjs/common": "2.6.5", "@ethereumjs/tx": "3.5.2", @@ -30561,10 +30706,10 @@ "eth-lib": "0.2.8", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-utils": "1.10.4" }, "dependencies": { "bn.js": { @@ -30590,24 +30735,24 @@ } }, "web3-eth-contract": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.3.tgz", - "integrity": "sha512-Y2CW61dCCyY4IoUMD4JsEQWrILX4FJWDWC/Txx/pr3K/+fGsBGvS9kWQN5EsVXOp4g7HoFOfVh9Lf7BmVVSRmg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.4.tgz", + "integrity": "sha512-Q8PfolOJ4eV9TvnTj1TGdZ4RarpSLmHnUnzVxZ/6/NiTfe4maJz99R0ISgwZkntLhLRtw0C7LRJuklzGYCNN3A==", "requires": { "@types/bn.js": "^5.1.1", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-utils": "1.10.4" }, "dependencies": { "@types/bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", "requires": { "@types/node": "*" } @@ -30615,40 +30760,40 @@ } }, "web3-eth-ens": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.3.tgz", - "integrity": "sha512-hR+odRDXGqKemw1GFniKBEXpjYwLgttTES+bc7BfTeoUyUZXbyDHe5ifC+h+vpzxh4oS0TnfcIoarK0Z9tFSiQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.4.tgz", + "integrity": "sha512-LLrvxuFeVooRVZ9e5T6OWKVflHPFgrVjJ/jtisRWcmI7KN/b64+D/wJzXqgmp6CNsMQcE7rpmf4CQmJCrTdsgg==", "requires": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-promievent": "1.10.3", - "web3-eth-abi": "1.10.3", - "web3-eth-contract": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-promievent": "1.10.4", + "web3-eth-abi": "1.10.4", + "web3-eth-contract": "1.10.4", + "web3-utils": "1.10.4" } }, "web3-eth-iban": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.3.tgz", - "integrity": "sha512-ZCfOjYKAjaX2TGI8uif5ah+J3BYFuo+47JOIV1RIz2l7kD9VfnxvRH5UiQDRyMALQC7KFd2hUqIEtHklapNyKA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.4.tgz", + "integrity": "sha512-0gE5iNmOkmtBmbKH2aTodeompnNE8jEyvwFJ6s/AF6jkw9ky9Op9cqfzS56AYAbrqEFuClsqB/AoRves7LDELw==", "requires": { "bn.js": "^5.2.1", - "web3-utils": "1.10.3" + "web3-utils": "1.10.4" } }, "web3-eth-personal": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.3.tgz", - "integrity": "sha512-avrQ6yWdADIvuNQcFZXmGLCEzulQa76hUOuVywN7O3cklB4nFc/Gp3yTvD3bOAaE7DhjLQfhUTCzXL7WMxVTsw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.4.tgz", + "integrity": "sha512-BRa/hs6jU1hKHz+AC/YkM71RP3f0Yci1dPk4paOic53R4ZZG4MgwKRkJhgt3/GPuPliwS46f/i5A7fEGBT4F9w==", "requires": { "@types/node": "^12.12.6", - "web3-core": "1.10.3", - "web3-core-helpers": "1.10.3", - "web3-core-method": "1.10.3", - "web3-net": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-helpers": "1.10.4", + "web3-core-method": "1.10.4", + "web3-net": "1.10.4", + "web3-utils": "1.10.4" }, "dependencies": { "@types/node": { @@ -30659,13 +30804,13 @@ } }, "web3-net": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.3.tgz", - "integrity": "sha512-IoSr33235qVoI1vtKssPUigJU9Fc/Ph0T9CgRi15sx+itysmvtlmXMNoyd6Xrgm9LuM4CIhxz7yDzH93B79IFg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.4.tgz", + "integrity": "sha512-mKINnhOOnZ4koA+yV2OT5s5ztVjIx7IY9a03w6s+yao/BUn+Luuty0/keNemZxTr1E8Ehvtn28vbOtW7Ids+Ow==", "requires": { - "web3-core": "1.10.3", - "web3-core-method": "1.10.3", - "web3-utils": "1.10.3" + "web3-core": "1.10.4", + "web3-core-method": "1.10.4", + "web3-utils": "1.10.4" } }, "web3-provider-engine": { @@ -30749,14 +30894,14 @@ } }, "web3-providers-http": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.3.tgz", - "integrity": "sha512-6dAgsHR3MxJ0Qyu3QLFlQEelTapVfWNTu5F45FYh8t7Y03T1/o+YAkVxsbY5AdmD+y5bXG/XPJ4q8tjL6MgZHw==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.4.tgz", + "integrity": "sha512-m2P5Idc8hdiO0l60O6DSCPw0kw64Zgi0pMjbEFRmxKIck2Py57RQMu4bxvkxJwkF06SlGaEQF8rFZBmuX7aagQ==", "requires": { "abortcontroller-polyfill": "^1.7.5", "cross-fetch": "^4.0.0", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" }, "dependencies": { "cross-fetch": { @@ -30770,39 +30915,39 @@ } }, "web3-providers-ipc": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.3.tgz", - "integrity": "sha512-vP5WIGT8FLnGRfswTxNs9rMfS1vCbMezj/zHbBe/zB9GauBRTYVrUo2H/hVrhLg8Ut7AbsKZ+tCJ4mAwpKi2hA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.4.tgz", + "integrity": "sha512-YRF/bpQk9z3WwjT+A6FI/GmWRCASgd+gC0si7f9zbBWLXjwzYAKG73bQBaFRAHex1hl4CVcM5WUMaQXf3Opeuw==", "requires": { "oboe": "2.1.5", - "web3-core-helpers": "1.10.3" + "web3-core-helpers": "1.10.4" } }, "web3-providers-ws": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.3.tgz", - "integrity": "sha512-/filBXRl48INxsh6AuCcsy4v5ndnTZ/p6bl67kmO9aK1wffv7CT++DrtclDtVMeDGCgB3van+hEf9xTAVXur7Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.4.tgz", + "integrity": "sha512-j3FBMifyuFFmUIPVQR4pj+t5ILhAexAui0opgcpu9R5LxQrLRUZxHSnU+YO25UycSOa/NAX8A+qkqZNpcFAlxA==", "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.10.3", + "web3-core-helpers": "1.10.4", "websocket": "^1.0.32" } }, "web3-shh": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.3.tgz", - "integrity": "sha512-cAZ60CPvs9azdwMSQ/PSUdyV4PEtaW5edAZhu3rCXf6XxQRliBboic+AvwUvB6j3eswY50VGa5FygfVmJ1JVng==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.4.tgz", + "integrity": "sha512-cOH6iFFM71lCNwSQrC3niqDXagMqrdfFW85hC9PFUrAr3PUrIem8TNstTc3xna2bwZeWG6OBy99xSIhBvyIACw==", "requires": { - "web3-core": "1.10.3", - "web3-core-method": "1.10.3", - "web3-core-subscriptions": "1.10.3", - "web3-net": "1.10.3" + "web3-core": "1.10.4", + "web3-core-method": "1.10.4", + "web3-core-subscriptions": "1.10.4", + "web3-net": "1.10.4" } }, "web3-utils": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.3.tgz", - "integrity": "sha512-OqcUrEE16fDBbGoQtZXWdavsPzbGIDc5v3VrRTZ0XrIpefC/viZ1ZU9bGEemazyS0catk/3rkOOxpzTfY+XsyQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", "requires": { "@ethereumjs/util": "^8.1.0", "bn.js": "^5.2.1", @@ -30815,14 +30960,14 @@ }, "dependencies": { "ethereum-cryptography": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", - "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz", + "integrity": "sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==", "requires": { - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1" + "@noble/curves": "1.3.0", + "@noble/hashes": "1.3.3", + "@scure/bip32": "1.3.3", + "@scure/bip39": "1.2.2" } } } @@ -30847,19 +30992,19 @@ "dev": true }, "webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", + "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.11.5", "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", @@ -30873,7 +31018,7 @@ "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", + "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -31169,9 +31314,9 @@ "dev": true }, "xss": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", - "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", "requires": { "commander": "^2.20.3", "cssfilter": "0.0.10" diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index b2ad77b9301e..7ae87ff82362 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -19,17 +19,17 @@ "eslint": "eslint js/**" }, "dependencies": { - "@fortawesome/fontawesome-free": "^6.4.2", - "@amplitude/analytics-browser": "^2.3.3", + "@fortawesome/fontawesome-free": "^6.5.1", + "@amplitude/analytics-browser": "^2.5.2", "@tarekraafat/autocomplete.js": "^10.2.7", "@walletconnect/web3-provider": "^1.8.0", "assert": "^2.1.0", "bignumber.js": "^9.1.2", "bootstrap": "^4.6.0", - "chart.js": "^4.4.0", + "chart.js": "^4.4.2", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", - "core-js": "^3.33.2", + "core-js": "^3.36.0", "crypto-browserify": "^3.12.0", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", @@ -56,55 +56,55 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.4.3", + "luxon": "^3.4.4", "malihu-custom-scrollbar-plugin": "3.1.5", - "mixpanel-browser": "^2.47.0", - "moment": "^2.29.4", + "mixpanel-browser": "^2.49.0", + "moment": "^2.30.1", "nanomorph": "^5.4.0", "numeral": "^2.0.6", "os-browserify": "^0.3.0", "path-parser": "^6.1.0", "phoenix": "file:../../../deps/phoenix", "phoenix_html": "file:../../../deps/phoenix_html", - "photoswipe": "^5.4.2", + "photoswipe": "^5.4.3", "pikaday": "^1.8.2", "popper.js": "^1.14.7", "reduce-reducers": "^1.0.4", - "redux": "^4.2.1", + "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.9.0", + "sweetalert2": "^11.10.5", "urijs": "^1.19.11", "url": "^0.11.3", "util": "^0.12.5", "viewerjs": "^1.11.6", - "web3": "^1.10.3", + "web3": "^1.10.4", "web3modal": "^1.9.12", - "xss": "^1.0.14" + "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "autoprefixer": "^10.4.16", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "autoprefixer": "^10.4.18", "babel-loader": "^9.1.3", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "eslint": "^8.52.0", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^6.10.0", + "css-minimizer-webpack-plugin": "^6.0.0", + "eslint": "^8.57.0", "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6.1.1", "file-loader": "^6.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "mini-css-extract-plugin": "^2.7.6", - "postcss": "^8.4.31", - "postcss-loader": "^7.3.3", - "sass": "^1.69.5", - "sass-loader": "^13.3.2", - "style-loader": "^3.3.3", - "webpack": "^5.89.0", + "mini-css-extract-plugin": "^2.8.1", + "postcss": "^8.4.35", + "postcss-loader": "^8.1.1", + "sass": "^1.71.1", + "sass-loader": "^14.1.1", + "style-loader": "^3.3.4", + "webpack": "^5.90.3", "webpack-cli": "^5.1.4" }, "jest": { diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index 0ab24f48cd5a..f733afcc59c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -13,11 +13,12 @@ defmodule BlockScoutWeb.ApiRouter do Router for API """ use BlockScoutWeb, :router - alias BlockScoutWeb.{AddressTransactionController, APIKeyV2Router, SmartContractsApiV2Router} + alias BlockScoutWeb.{AddressTransactionController, APIKeyV2Router, SmartContractsApiV2Router, UtilsApiV2Router} alias BlockScoutWeb.Plug.{CheckAccountAPI, CheckApiV2, RateLimit} forward("/v2/smart-contracts", SmartContractsApiV2Router) forward("/v2/key", APIKeyV2Router) + forward("/v2/utils", UtilsApiV2Router) pipeline :api do plug(BlockScoutWeb.Plug.Logger, application: :api) @@ -181,6 +182,7 @@ defmodule BlockScoutWeb.ApiRouter do pipe_through(:api_v2_no_session) post("/token-info", V2.ImportController, :import_token_info) + get("/smart-contracts/:address_hash_param", V2.ImportController, :try_to_search_contract) end scope "/v2", as: :api_v2 do @@ -200,12 +202,17 @@ defmodule BlockScoutWeb.ApiRouter do scope "/transactions" do get("/", V2.TransactionController, :transactions) get("/watchlist", V2.TransactionController, :watchlist_transactions) + get("/stats", V2.TransactionController, :stats) - if System.get_env("CHAIN_TYPE") == "polygon_zkevm" do - get("/zkevm-batch/:batch_number", V2.TransactionController, :zkevm_batch) + if Application.compile_env(:explorer, :chain_type) == "polygon_zkevm" do + get("/zkevm-batch/:batch_number", V2.TransactionController, :polygon_zkevm_batch) end - if System.get_env("CHAIN_TYPE") == "suave" do + if Application.compile_env(:explorer, :chain_type) == "zksync" do + get("/zksync-batch/:batch_number", V2.TransactionController, :zksync_batch) + end + + if Application.compile_env(:explorer, :chain_type) == "suave" do get("/execution-node/:execution_node_hash_param", V2.TransactionController, :execution_node) end @@ -215,6 +222,11 @@ defmodule BlockScoutWeb.ApiRouter do get("/:transaction_hash_param/logs", V2.TransactionController, :logs) get("/:transaction_hash_param/raw-trace", V2.TransactionController, :raw_trace) get("/:transaction_hash_param/state-changes", V2.TransactionController, :state_changes) + get("/:transaction_hash_param/summary", V2.TransactionController, :summary) + + if Application.compile_env(:explorer, :chain_type) == "ethereum" do + get("/:transaction_hash_param/blobs", V2.TransactionController, :blobs) + end end scope "/blocks" do @@ -239,9 +251,15 @@ defmodule BlockScoutWeb.ApiRouter do get("/:address_hash_param/coin-balance-history", V2.AddressController, :coin_balance_history) get("/:address_hash_param/coin-balance-history-by-day", V2.AddressController, :coin_balance_history_by_day) get("/:address_hash_param/withdrawals", V2.AddressController, :withdrawals) + get("/:address_hash_param/nft", V2.AddressController, :nft_list) + get("/:address_hash_param/nft/collections", V2.AddressController, :nft_collections) end scope "/tokens" do + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + get("/bridged", V2.TokenController, :bridged_tokens_list) + end + get("/", V2.TokenController, :tokens_list) get("/:address_hash_param", V2.TokenController, :token) get("/:address_hash_param/counters", V2.TokenController, :counters) @@ -260,9 +278,18 @@ defmodule BlockScoutWeb.ApiRouter do get("/transactions/watchlist", V2.MainPageController, :watchlist_transactions) get("/indexing-status", V2.MainPageController, :indexing_status) - if System.get_env("CHAIN_TYPE") == "polygon_zkevm" do - get("/zkevm/batches/confirmed", V2.ZkevmController, :batches_confirmed) - get("/zkevm/batches/latest-number", V2.ZkevmController, :batch_latest_number) + if Application.compile_env(:explorer, :chain_type) == "optimism" do + get("/optimism-deposits", V2.MainPageController, :optimism_deposits) + end + + if Application.compile_env(:explorer, :chain_type) == "polygon_zkevm" do + get("/zkevm/batches/confirmed", V2.PolygonZkevmController, :batches_confirmed) + get("/zkevm/batches/latest-number", V2.PolygonZkevmController, :batch_latest_number) + end + + if Application.compile_env(:explorer, :chain_type) == "zksync" do + get("/zksync/batches/confirmed", V2.ZkSyncController, :batches_confirmed) + get("/zksync/batches/latest-number", V2.ZkSyncController, :batch_latest_number) end end @@ -272,11 +299,25 @@ defmodule BlockScoutWeb.ApiRouter do scope "/charts" do get("/transactions", V2.StatsController, :transactions_chart) get("/market", V2.StatsController, :market_chart) + get("/secondary-coin-market", V2.StatsController, :secondary_coin_market_chart) + end + end + + scope "/optimism" do + if Application.compile_env(:explorer, :chain_type) == "optimism" do + get("/txn-batches", V2.OptimismController, :txn_batches) + get("/txn-batches/count", V2.OptimismController, :txn_batches_count) + get("/output-roots", V2.OptimismController, :output_roots) + get("/output-roots/count", V2.OptimismController, :output_roots_count) + get("/deposits", V2.OptimismController, :deposits) + get("/deposits/count", V2.OptimismController, :deposits_count) + get("/withdrawals", V2.OptimismController, :withdrawals) + get("/withdrawals/count", V2.OptimismController, :withdrawals_count) end end scope "/polygon-edge" do - if System.get_env("CHAIN_TYPE") == "polygon_edge" do + if Application.compile_env(:explorer, :chain_type) == "polygon_edge" do get("/deposits", V2.PolygonEdgeController, :deposits) get("/deposits/count", V2.PolygonEdgeController, :deposits_count) get("/withdrawals", V2.PolygonEdgeController, :withdrawals) @@ -284,16 +325,77 @@ defmodule BlockScoutWeb.ApiRouter do end end + scope "/shibarium" do + if Application.compile_env(:explorer, :chain_type) == "shibarium" do + get("/deposits", V2.ShibariumController, :deposits) + get("/deposits/count", V2.ShibariumController, :deposits_count) + get("/withdrawals", V2.ShibariumController, :withdrawals) + get("/withdrawals/count", V2.ShibariumController, :withdrawals_count) + end + end + scope "/withdrawals" do get("/", V2.WithdrawalController, :withdrawals_list) get("/counters", V2.WithdrawalController, :withdrawals_counters) end scope "/zkevm" do - if System.get_env("CHAIN_TYPE") == "polygon_zkevm" do - get("/batches", V2.ZkevmController, :batches) - get("/batches/count", V2.ZkevmController, :batches_count) - get("/batches/:batch_number", V2.ZkevmController, :batch) + if Application.compile_env(:explorer, :chain_type) == "polygon_zkevm" do + get("/batches", V2.PolygonZkevmController, :batches) + get("/batches/count", V2.PolygonZkevmController, :batches_count) + get("/batches/:batch_number", V2.PolygonZkevmController, :batch) + get("/deposits", V2.PolygonZkevmController, :deposits) + get("/deposits/count", V2.PolygonZkevmController, :deposits_count) + get("/withdrawals", V2.PolygonZkevmController, :withdrawals) + get("/withdrawals/count", V2.PolygonZkevmController, :withdrawals_count) + end + end + + scope "/proxy" do + scope "/noves-fi" do + get("/transactions/:transaction_hash_param", V2.Proxy.NovesFiController, :transaction) + + get("/addresses/:address_hash_param/transactions", V2.Proxy.NovesFiController, :address_transactions) + + get("/transaction-descriptions", V2.Proxy.NovesFiController, :describe_transactions) + end + + scope "/account-abstraction" do + get("/operations/:operation_hash_param", V2.Proxy.AccountAbstractionController, :operation) + get("/operations/:operation_hash_param/summary", V2.Proxy.AccountAbstractionController, :summary) + get("/bundlers/:address_hash_param", V2.Proxy.AccountAbstractionController, :bundler) + get("/bundlers", V2.Proxy.AccountAbstractionController, :bundlers) + get("/factories/:address_hash_param", V2.Proxy.AccountAbstractionController, :factory) + get("/factories", V2.Proxy.AccountAbstractionController, :factories) + get("/paymasters/:address_hash_param", V2.Proxy.AccountAbstractionController, :paymaster) + get("/paymasters", V2.Proxy.AccountAbstractionController, :paymasters) + get("/accounts/:address_hash_param", V2.Proxy.AccountAbstractionController, :account) + get("/accounts", V2.Proxy.AccountAbstractionController, :accounts) + get("/bundles", V2.Proxy.AccountAbstractionController, :bundles) + get("/operations", V2.Proxy.AccountAbstractionController, :operations) + end + end + + scope "/blobs" do + if Application.compile_env(:explorer, :chain_type) == "ethereum" do + get("/:blob_hash_param", V2.BlobController, :blob) + end + end + + scope "/validators" do + if Application.compile_env(:explorer, :chain_type) == "stability" do + scope "/stability" do + get("/", V2.ValidatorController, :stability_validators_list) + get("/counters", V2.ValidatorController, :stability_validators_counters) + end + end + end + + scope "/zksync" do + if Application.compile_env(:explorer, :chain_type) == "zksync" do + get("/batches", V2.ZkSyncController, :batches) + get("/batches/count", V2.ZkSyncController, :batches_count) + get("/batches/:batch_number", V2.ZkSyncController, :batch) end end end diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index 2d84cc8b101b..f323ef8e959d 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.Application do alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.Counters.{BlocksIndexedCounter, InternalTransactionsIndexedCounter} alias BlockScoutWeb.{Endpoint, Prometheus} - alias BlockScoutWeb.RealtimeEventHandler + alias BlockScoutWeb.{MainPageRealtimeEventHandler, RealtimeEventHandler, SmartContractRealtimeEventHandler} def start(_type, _args) do import Supervisor @@ -34,7 +34,9 @@ defmodule BlockScoutWeb.Application do {Phoenix.PubSub, name: BlockScoutWeb.PubSub}, child_spec(Endpoint, []), {Absinthe.Subscription, Endpoint}, + {MainPageRealtimeEventHandler, name: MainPageRealtimeEventHandler}, {RealtimeEventHandler, name: RealtimeEventHandler}, + {SmartContractRealtimeEventHandler, name: SmartContractRealtimeEventHandler}, {BlocksIndexedCounter, name: BlocksIndexedCounter}, {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter} ] diff --git a/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex b/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex index a158813b9921..78194bd8df7c 100644 --- a/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex @@ -18,7 +18,7 @@ defmodule BlockScoutWeb.CaptchaHelper do case HTTPoison.post("https://www.google.com/recaptcha/api/siteverify", body, headers, []) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> case Jason.decode!(body) do - %{"success" => true} = resp -> is_success?(resp) + %{"success" => true} = resp -> success?(resp) _ -> false end @@ -27,11 +27,11 @@ defmodule BlockScoutWeb.CaptchaHelper do end end - defp is_success?(%{"score" => score}) do + defp success?(%{"score" => score}) do check_recaptcha_v3_score(score) end - defp is_success?(_resp), do: true + defp success?(_resp), do: true defp check_recaptcha_v3_score(score) do if score >= 0.5 do diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index dc3bcbe83780..d0e110218e15 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -17,13 +17,18 @@ defmodule BlockScoutWeb.Chain do import Explorer.Helper, only: [parse_integer: 1] + alias BlockScoutWeb.PagingHelper + alias Ecto.Association.NotLoaded + alias Explorer.Chain.UserOperation alias Explorer.Account.{TagAddress, TagTransaction, WatchlistAddress} + alias Explorer.Chain.Beacon.Reader, as: BeaconReader alias Explorer.Chain.Block.Reward alias Explorer.Chain.{ Address, Address.CoinBalance, Address.CurrentTokenBalance, + Beacon.Blob, Block, Hash, InternalTransaction, @@ -34,11 +39,14 @@ defmodule BlockScoutWeb.Chain do TokenTransfer, Transaction, Transaction.StateChange, - Wei, - Withdrawal + UserOperation, + Wei } - alias Explorer.Chain.Zkevm.TransactionBatch + alias Explorer.Chain.Optimism.Deposit, as: OptimismDeposit + alias Explorer.Chain.Optimism.OutputRoot, as: OptimismOutputRoot + + alias Explorer.Chain.PolygonZkevm.TransactionBatch alias Explorer.PagingOptions defimpl Poison.Encoder, for: Decimal do @@ -53,7 +61,7 @@ defmodule BlockScoutWeb.Chain do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @address_hash_len 40 - @tx_block_hash_len 64 + @full_hash_len 64 def default_paging_options do @default_paging_options @@ -79,20 +87,21 @@ defmodule BlockScoutWeb.Chain do end end - @spec from_param(String.t()) :: {:ok, Address.t() | Block.t() | Transaction.t()} | {:error, :not_found} + @spec from_param(String.t()) :: + {:ok, Address.t() | Block.t() | Transaction.t() | UserOperation.t() | Blob.t()} | {:error, :not_found} def from_param(param) def from_param("0x" <> number_string = param) when byte_size(number_string) == @address_hash_len, do: address_from_param(param) - def from_param("0x" <> number_string = param) when byte_size(number_string) == @tx_block_hash_len, - do: block_or_transaction_from_param(param) + def from_param("0x" <> number_string = param) when byte_size(number_string) == @full_hash_len, + do: block_or_transaction_or_operation_or_blob_from_param(param) def from_param(param) when byte_size(param) == @address_hash_len, do: address_from_param("0x" <> param) - def from_param(param) when byte_size(param) == @tx_block_hash_len, - do: block_or_transaction_from_param("0x" <> param) + def from_param(param) when byte_size(param) == @full_hash_len, + do: block_or_transaction_or_operation_or_blob_from_param("0x" <> param) def from_param(string) when is_binary(string) do case param_to_block_number(string) do @@ -101,16 +110,13 @@ defmodule BlockScoutWeb.Chain do end end - @spec next_page_params(any, any, any, any) :: nil | map - def next_page_params(next_page, list, params, is_ctb_with_fiat_value \\ false) + @spec next_page_params(any, list(), map(), (any -> map())) :: nil | map + def next_page_params(next_page, list, params, paging_function \\ &paging_params/1) def next_page_params([], _list, _params, _), do: nil - def next_page_params(_, list, params, is_ctb_with_fiat_value) do - paging_params = - if is_ctb_with_fiat_value, - do: paging_params_with_fiat_value(List.last(list)), - else: paging_params(List.last(list)) + def next_page_params(_, list, params, paging_function) do + paging_params = paging_function.(List.last(list)) next_page_params = Map.merge(params, paging_params) current_items_count_string = Map.get(next_page_params, "items_count") @@ -143,6 +149,34 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{ + "fee" => fee_string, + "value" => value_string, + "block_number" => block_number_string, + "index" => index_string, + "inserted_at" => inserted_at_string, + "hash" => hash_string + }) do + with {:ok, hash} <- string_to_transaction_hash(hash_string), + {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string) do + [ + paging_options: %{ + @default_paging_options + | key: %{ + fee: decimal_parse(fee_string), + value: decimal_parse(value_string), + block_number: parse_integer(block_number_string), + index: parse_integer(index_string), + inserted_at: inserted_at, + hash: hash + } + } + ] + else + _ -> [paging_options: @default_paging_options] + end + end + def paging_options(%{ "address_hash" => address_hash_string, "tx_hash" => tx_hash_string, @@ -308,6 +342,16 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: {index}}] end + def paging_options(%{"nonce" => nonce_string}) when is_binary(nonce_string) do + case Integer.parse(nonce_string) do + {nonce, ""} -> + [paging_options: %{@default_paging_options | key: {nonce}}] + + _ -> + [paging_options: @default_paging_options] + end + end + def paging_options(%{"number" => number_string}) when is_binary(number_string) do case Integer.parse(number_string) do {number, ""} -> @@ -318,6 +362,10 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{"nonce" => nonce}) when is_integer(nonce) do + [paging_options: %{@default_paging_options | key: {nonce}}] + end + def paging_options(%{"number" => number}) when is_integer(number) do [paging_options: %{@default_paging_options | key: {number}}] end @@ -355,8 +403,17 @@ defmodule BlockScoutWeb.Chain do end end - def paging_options(%{"smart_contract_id" => id}) do - [paging_options: %{@default_paging_options | key: {id}}] + def paging_options(%{"smart_contract_id" => id_str} = params) do + transactions_count = parse_integer(params["tx_count"]) + coin_balance = parse_integer(params["coin_balance"]) + id = parse_integer(id_str) + + [ + paging_options: %{ + @default_paging_options + | key: %{id: id, transactions_count: transactions_count, fetched_coin_balance: coin_balance} + } + ] end def paging_options(%{"items_count" => items_count_string, "state_changes" => _}) when is_binary(items_count_string) do @@ -366,6 +423,16 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{"l1_block_number" => block_number, "tx_hash" => tx_hash}) do + with {block_number, ""} <- Integer.parse(block_number), + {:ok, tx_hash} <- string_to_transaction_hash(tx_hash) do + [paging_options: %{@default_paging_options | key: {block_number, tx_hash}}] + else + _ -> + [paging_options: @default_paging_options] + end + end + # clause for Polygon Edge Deposits and Withdrawals and for account's entities pagination def paging_options(%{"id" => id_string}) when is_binary(id_string) do case Integer.parse(id_string) do @@ -382,6 +449,37 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: {id}}] end + def paging_options(%{ + "token_contract_address_hash" => token_contract_address_hash, + "token_id" => token_id, + "token_type" => token_type + }) do + [paging_options: %{@default_paging_options | key: {token_contract_address_hash, token_id, token_type}}] + end + + def paging_options(%{"token_contract_address_hash" => token_contract_address_hash, "token_type" => token_type}) do + [paging_options: %{@default_paging_options | key: {token_contract_address_hash, token_type}}] + end + + # Clause for `Explorer.Chain.Stability.Validator`, + # returned by `BlockScoutWeb.API.V2.ValidatorController.stability_validators_list/2` (`/api/v2/validators/stability`) + def paging_options(%{ + "state" => state, + "address_hash" => address_hash_string, + "blocks_validated" => blocks_validated_string + }) do + [ + paging_options: %{ + @default_paging_options + | key: %{ + address_hash: parse_address_hash(address_hash_string), + blocks_validated: parse_integer(blocks_validated_string), + state: if(state in PagingHelper.allowed_stability_validators_states(), do: state) + } + } + ] + end + def paging_options(_params), do: [paging_options: @default_paging_options] def put_key_value_to_paging_options([paging_options: paging_options], key, value) do @@ -464,6 +562,13 @@ defmodule BlockScoutWeb.Chain do end end + defp parse_address_hash(address_hash_string) do + case Hash.Address.cast(address_hash_string) do + {:ok, address_hash} -> address_hash + _ -> nil + end + end + defp paging_params({%Address{hash: hash, fetched_coin_balance: fetched_coin_balance}, _}) do %{"hash" => hash, "fetched_coin_balance" => Decimal.to_string(fetched_coin_balance.value)} end @@ -485,6 +590,10 @@ defmodule BlockScoutWeb.Chain do } end + defp paging_params({%Token{} = token, _}) do + paging_params(token) + end + defp paging_params(%TagAddress{id: id}) do %{"id" => id} end @@ -544,14 +653,39 @@ defmodule BlockScoutWeb.Chain do %{"block_number" => block_number} end - defp paging_params(%SmartContract{} = smart_contract) do + defp paging_params(%SmartContract{address: %NotLoaded{}} = smart_contract) do %{"smart_contract_id" => smart_contract.id} end - defp paging_params(%Withdrawal{index: index}) do + defp paging_params(%OptimismDeposit{l1_block_number: l1_block_number, l2_transaction_hash: l2_tx_hash}) do + %{"l1_block_number" => l1_block_number, "tx_hash" => l2_tx_hash} + end + + defp paging_params(%OptimismOutputRoot{l2_output_index: index}) do %{"index" => index} end + defp paging_params(%SmartContract{} = smart_contract) do + %{ + "smart_contract_id" => smart_contract.id, + "tx_count" => smart_contract.address.transactions_count, + "coin_balance" => + smart_contract.address.fetched_coin_balance && Wei.to(smart_contract.address.fetched_coin_balance, :wei) + } + end + + defp paging_params(%{index: index}) do + %{"index" => index} + end + + defp paging_params(%{msg_nonce: nonce}) do + %{"nonce" => nonce} + end + + defp paging_params(%{l2_block_number: block_number}) do + %{"block_number" => block_number} + end + # clause for zkEVM batches pagination defp paging_params(%TransactionBatch{number: number}) do %{"number" => number} @@ -593,33 +727,49 @@ defmodule BlockScoutWeb.Chain do %{"id" => msg_id} end - defp paging_params_with_fiat_value(%CurrentTokenBalance{id: id, value: value} = ctb) do - %{"fiat_value" => ctb.fiat_value, "value" => value, "id" => id} + # clause for Shibarium Deposits + defp paging_params(%{l1_block_number: block_number}) do + %{"block_number" => block_number} end - defp block_or_transaction_from_param(param) do - with {:error, :not_found} <- transaction_from_param(param) do - hash_string_to_block(param) - end + # clause for Shibarium Withdrawals + defp paging_params(%{l2_block_number: block_number}) do + %{"block_number" => block_number} end - defp transaction_from_param(param) do - case string_to_transaction_hash(param) do - {:ok, hash} -> - hash_to_transaction(hash) + @spec paging_params_with_fiat_value(CurrentTokenBalance.t()) :: %{ + required(String.t()) => Decimal.t() | non_neg_integer() | nil + } + def paging_params_with_fiat_value(%CurrentTokenBalance{id: id, value: value} = ctb) do + %{"fiat_value" => ctb.fiat_value, "value" => value, "id" => id} + end - :error -> - {:error, :not_found} + defp block_or_transaction_or_operation_or_blob_from_param(param) do + with {:ok, hash} <- string_to_transaction_hash(param), + {:error, :not_found} <- hash_to_transaction(hash), + {:error, :not_found} <- hash_to_block(hash), + {:error, :not_found} <- hash_to_user_operation(hash), + {:error, :not_found} <- hash_to_blob(hash) do + {:error, :not_found} + else + :error -> {:error, :not_found} + res -> res end end - defp hash_string_to_block(hash_string) do - case string_to_block_hash(hash_string) do - {:ok, hash} -> - hash_to_block(hash) + defp hash_to_user_operation(hash) do + if UserOperation.enabled?() do + UserOperation.hash_to_user_operation(hash) + else + {:error, :not_found} + end + end - :error -> - {:error, :not_found} + defp hash_to_blob(hash) do + if Application.get_env(:explorer, :chain_type) == "ethereum" do + BeaconReader.blob(hash, false) + else + {:error, :not_found} end end diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index f0ead554c72d..8f95108bb2be 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -193,11 +193,13 @@ defmodule BlockScoutWeb.AddressChannel do ) do coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number) - rendered_coin_balance = AddressViewAPI.render("coin_balance.json", %{coin_balance: coin_balance}) + if coin_balance.value && coin_balance.delta do + rendered_coin_balance = AddressViewAPI.render("coin_balance.json", %{coin_balance: coin_balance}) - push(socket, "coin_balance", %{coin_balance: rendered_coin_balance}) + push(socket, "coin_balance", %{coin_balance: rendered_coin_balance}) - push_current_coin_balance(socket, block_number, coin_balance) + push_current_coin_balance(socket, block_number, coin_balance) + end {:noreply, socket} end @@ -207,19 +209,21 @@ defmodule BlockScoutWeb.AddressChannel do Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) - rendered_coin_balance = - View.render_to_string( - AddressCoinBalanceView, - "_coin_balances.html", - conn: socket, - coin_balance: coin_balance - ) + if coin_balance.value && coin_balance.delta do + rendered_coin_balance = + View.render_to_string( + AddressCoinBalanceView, + "_coin_balances.html", + conn: socket, + coin_balance: coin_balance + ) - push(socket, "coin_balance", %{ - coin_balance_html: rendered_coin_balance - }) + push(socket, "coin_balance", %{ + coin_balance_html: rendered_coin_balance + }) - push_current_coin_balance(socket, block_number, coin_balance) + push_current_coin_balance(socket, block_number, coin_balance) + end {:noreply, socket} end @@ -237,6 +241,7 @@ defmodule BlockScoutWeb.AddressChannel do push_current_token_balances(socket, address_current_token_balances, "erc_20", "ERC-20") push_current_token_balances(socket, address_current_token_balances, "erc_721", "ERC-721") push_current_token_balances(socket, address_current_token_balances, "erc_1155", "ERC-1155") + push_current_token_balances(socket, address_current_token_balances, "erc_404", "ERC-404") {:noreply, socket} end @@ -246,7 +251,20 @@ defmodule BlockScoutWeb.AddressChannel do end defp push_current_token_balances(socket, address_current_token_balances, event_postfix, token_type) do - filtered_ctbs = address_current_token_balances |> Enum.filter(fn ctb -> ctb.token_type == token_type end) + filtered_ctbs = + address_current_token_balances + |> Enum.filter(fn ctb -> ctb.token_type == token_type end) + |> Enum.sort_by( + fn ctb -> + value = + if ctb.token.decimals, + do: Decimal.div(ctb.value, Decimal.new(Integer.pow(10, Decimal.to_integer(ctb.token.decimals)))), + else: ctb.value + + {(ctb.token.fiat_value && Decimal.mult(value, ctb.token.fiat_value)) || Decimal.new(0), value} + end, + &sorter/2 + ) push(socket, "updated_token_balances_" <> event_postfix, %{ token_balances: @@ -257,6 +275,15 @@ defmodule BlockScoutWeb.AddressChannel do }) end + defp sorter({fiat_value_1, value_1}, {fiat_value_2, value_2}) do + case {Decimal.compare(fiat_value_1, fiat_value_2), Decimal.compare(value_1, value_2)} do + {:gt, _} -> true + {:eq, :gt} -> true + {:eq, :eq} -> true + _ -> false + end + end + def push_current_coin_balance( %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket, block_number, diff --git a/apps/block_scout_web/lib/block_scout_web/channels/optimism_deposit_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/optimism_deposit_channel.ex new file mode 100644 index 000000000000..3f2c513f9b1c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/channels/optimism_deposit_channel.ex @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.OptimismDepositChannel do + @moduledoc """ + Establishes pub/sub channel for live updates of Optimism deposit events. + """ + use BlockScoutWeb, :channel + + intercept(["deposits"]) + + def join("optimism_deposits:new_deposits", _params, socket) do + {:ok, %{}, socket} + end + + def handle_out( + "deposits", + %{deposits: deposits}, + %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket + ) do + push(socket, "deposits", %{deposits: Enum.count(deposits)}) + + {:noreply, socket} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/channels/zkevm_confirmed_batch_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/polygon_zkevm_confirmed_batch_channel.ex similarity index 73% rename from apps/block_scout_web/lib/block_scout_web/channels/zkevm_confirmed_batch_channel.ex rename to apps/block_scout_web/lib/block_scout_web/channels/polygon_zkevm_confirmed_batch_channel.ex index 9007f1176412..0b80fb53580f 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/zkevm_confirmed_batch_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/polygon_zkevm_confirmed_batch_channel.ex @@ -1,10 +1,10 @@ -defmodule BlockScoutWeb.ZkevmConfirmedBatchChannel do +defmodule BlockScoutWeb.PolygonZkevmConfirmedBatchChannel do @moduledoc """ Establishes pub/sub channel for live updates of zkEVM confirmed batch events. """ use BlockScoutWeb, :channel - alias BlockScoutWeb.API.V2.ZkevmView + alias BlockScoutWeb.API.V2.PolygonZkevmView intercept(["new_zkevm_confirmed_batch"]) @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.ZkevmConfirmedBatchChannel do %{batch: batch}, %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket ) do - rendered_batch = ZkevmView.render("zkevm_batch.json", %{batch: batch, socket: nil}) + rendered_batch = PolygonZkevmView.render("zkevm_batch.json", %{batch: batch, socket: nil}) push(socket, "new_zkevm_confirmed_batch", %{ batch: rendered_batch diff --git a/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex b/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex index 6060428a7981..e357674a3c11 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex @@ -5,6 +5,7 @@ defmodule BlockScoutWeb.UserSocket do channel("addresses:*", BlockScoutWeb.AddressChannel) channel("blocks:*", BlockScoutWeb.BlockChannel) channel("exchange_rate:*", BlockScoutWeb.ExchangeRateChannel) + channel("optimism_deposits:*", BlockScoutWeb.OptimismDepositChannel) channel("rewards:*", BlockScoutWeb.RewardChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("tokens:*", BlockScoutWeb.TokenChannel) diff --git a/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex b/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex index 159993433e35..740b716dc322 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex @@ -7,10 +7,11 @@ defmodule BlockScoutWeb.UserSocketV2 do channel("addresses:*", BlockScoutWeb.AddressChannel) channel("blocks:*", BlockScoutWeb.BlockChannel) channel("exchange_rate:*", BlockScoutWeb.ExchangeRateChannel) + channel("optimism_deposits:*", BlockScoutWeb.OptimismDepositChannel) channel("rewards:*", BlockScoutWeb.RewardChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("tokens:*", BlockScoutWeb.TokenChannel) - channel("zkevm_batches:*", BlockScoutWeb.ZkevmConfirmedBatchChannel) + channel("zkevm_batches:*", BlockScoutWeb.PolygonZkevmConfirmedBatchChannel) def connect(_params, socket) do {:ok, socket} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex index 724378cc69d6..af723cacf13c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex @@ -146,12 +146,15 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do "ERC-721" => %{ "incoming" => watch_erc_721_input, "outcoming" => watch_erc_721_output - } - # , + }, # "ERC-1155" => %{ # "incoming" => watch_erc_1155_input, # "outcoming" => watch_erc_1155_output - # } + # }, + "ERC-404" => %{ + "incoming" => watch_erc_404_input, + "outcoming" => watch_erc_404_output + } }, "notification_methods" => %{ "email" => notify_email @@ -167,6 +170,8 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do watch_erc_721_output: watch_erc_721_output, watch_erc_1155_input: watch_erc_721_input, watch_erc_1155_output: watch_erc_721_output, + watch_erc_404_input: watch_erc_404_input, + watch_erc_404_output: watch_erc_404_output, notify_email: notify_email, address_hash: address_hash } @@ -202,12 +207,15 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do "ERC-721" => %{ "incoming" => watch_erc_721_input, "outcoming" => watch_erc_721_output - } - # , + }, # "ERC-1155" => %{ # "incoming" => watch_erc_1155_input, # "outcoming" => watch_erc_1155_output - # } + # }, + "ERC-404" => %{ + "incoming" => watch_erc_404_input, + "outcoming" => watch_erc_404_output + } }, "notification_methods" => %{ "email" => notify_email @@ -224,6 +232,8 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do watch_erc_721_output: watch_erc_721_output, watch_erc_1155_input: watch_erc_721_input, watch_erc_1155_output: watch_erc_721_output, + watch_erc_404_input: watch_erc_404_input, + watch_erc_404_output: watch_erc_404_output, notify_email: notify_email, address_hash: address_hash } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex index 445b7ca48679..a43c5c7b2b1c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex @@ -15,7 +15,7 @@ defmodule BlockScoutWeb.AddressContractController do necessity_by_association: %{ :contracts_creation_internal_transaction => :optional, :names => :optional, - :smart_contract => :optional, + [smart_contract: :smart_contract_additional_sources] => :optional, :token => :optional, :contracts_creation_transaction => :optional } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_controller.ex index 5a00556fac09..60f4720aef37 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_controller.ex @@ -2,7 +2,6 @@ defmodule BlockScoutWeb.AddressContractVerificationController do use BlockScoutWeb, :controller alias BlockScoutWeb.Controller - alias Explorer.Chain alias Explorer.Chain.Events.Publisher, as: EventsPublisher alias Explorer.Chain.SmartContract alias Explorer.SmartContract.{CompilerVersion, Solidity.CodeCompiler} @@ -12,7 +11,7 @@ defmodule BlockScoutWeb.AddressContractVerificationController do alias Explorer.ThirdPartyIntegrations.Sourcify def new(conn, %{"address_id" => address_hash_string}) do - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do address_contract_path = conn |> address_contract_path(:index, address_hash_string) @@ -122,7 +121,7 @@ defmodule BlockScoutWeb.AddressContractVerificationController do json_file = PublishHelper.get_one_json(files_array) if json_file do - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do EventsPublisher.broadcast( PublishHelper.prepare_verification_error( "This contract already verified in Blockscout.", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_flattened_code_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_flattened_code_controller.ex index fab2192cfae0..72cd3aeb3bca 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_flattened_code_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_flattened_code_controller.ex @@ -2,12 +2,11 @@ defmodule BlockScoutWeb.AddressContractVerificationViaFlattenedCodeController do use BlockScoutWeb, :controller alias BlockScoutWeb.Controller - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.{CompilerVersion, Solidity.CodeCompiler, Solidity.PublisherWorker} def new(conn, %{"address_id" => address_hash_string}) do - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do address_contract_path = conn |> address_contract_path(:index, address_hash_string) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_json_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_json_controller.ex index 87a75ac7cb7e..b41ae1517fdc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_json_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_json_controller.ex @@ -2,7 +2,6 @@ defmodule BlockScoutWeb.AddressContractVerificationViaJsonController do use BlockScoutWeb, :controller alias BlockScoutWeb.Controller - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.Solidity.PublishHelper alias Explorer.ThirdPartyIntegrations.Sourcify @@ -13,7 +12,7 @@ defmodule BlockScoutWeb.AddressContractVerificationViaJsonController do |> address_contract_path(:index, address_hash_string) |> Controller.full_path() - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do redirect(conn, to: address_contract_path) else case Sourcify.check_by_address(address_hash_string) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_multi_part_files_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_multi_part_files_controller.ex index 7d0f819e96e2..78f6ea6b99c4 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_multi_part_files_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_multi_part_files_controller.ex @@ -2,12 +2,11 @@ defmodule BlockScoutWeb.AddressContractVerificationViaMultiPartFilesController d use BlockScoutWeb, :controller alias BlockScoutWeb.Controller - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.{CompilerVersion, Solidity.CodeCompiler} def new(conn, %{"address_id" => address_hash_string}) do - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do address_contract_path = conn |> address_contract_path(:index, address_hash_string) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_standard_json_input_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_standard_json_input_controller.ex index 1938269f79ef..37447acd8195 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_standard_json_input_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_via_standard_json_input_controller.ex @@ -2,12 +2,11 @@ defmodule BlockScoutWeb.AddressContractVerificationViaStandardJsonInputControlle use BlockScoutWeb, :controller alias BlockScoutWeb.Controller - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.CompilerVersion def new(conn, %{"address_id" => address_hash_string}) do - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do address_contract_path = conn |> address_contract_path(:index, address_hash_string) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_vyper_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_vyper_controller.ex index 19123f940057..6c978901cd0a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_vyper_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_verification_vyper_controller.ex @@ -2,12 +2,11 @@ defmodule BlockScoutWeb.AddressContractVerificationVyperController do use BlockScoutWeb, :controller alias BlockScoutWeb.Controller - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.{CompilerVersion, Vyper.PublisherWorker} def new(conn, %{"address_id" => address_hash_string}) do - if Chain.smart_contract_fully_verified?(address_hash_string) do + if SmartContract.verified_with_full_match?(address_hash_string) do address_contract_path = conn |> address_contract_path(:index, address_hash_string) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index c352cb8e783b..3e50dff92ba7 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.AddressController do alias Explorer.{Chain, Market} alias Explorer.Chain.Address.Counters - alias Explorer.Chain.Wei + alias Explorer.Chain.{Address, Wei} alias Indexer.Fetcher.CoinBalanceOnDemand alias Phoenix.View @@ -24,7 +24,7 @@ defmodule BlockScoutWeb.AddressController do addresses = params |> paging_options() - |> Chain.list_top_addresses() + |> Address.list_top_addresses() {addresses_page, next_page} = split_list_by_page(addresses) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex index fd7ee5a60970..b76d713d3927 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -11,6 +11,7 @@ defmodule BlockScoutWeb.AddressLogsController do alias BlockScoutWeb.{AccessHelper, AddressLogsView, Controller} alias Explorer.{Chain, Market} + alias Explorer.Chain.Address alias Indexer.Fetcher.CoinBalanceOnDemand alias Phoenix.View @@ -18,7 +19,7 @@ defmodule BlockScoutWeb.AddressLogsController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - :ok <- Chain.check_address_exists(address_hash), + :ok <- Address.check_address_exists(address_hash), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do logs_plus_one = Chain.address_to_logs(address_hash, false, paging_options(params)) {results, next_page} = split_list_by_page(logs_plus_one) @@ -78,7 +79,7 @@ defmodule BlockScoutWeb.AddressLogsController do def search_logs(conn, %{"topic" => topic, "address_id" => address_hash_string} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - :ok <- Chain.check_address_exists(address_hash) do + :ok <- Address.check_address_exists(address_hash) do topic = String.trim(topic) formatted_topic = if String.starts_with?(topic, "0x"), do: topic, else: "0x" <> topic diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex index 3fa058758e6d..d130043d8700 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex @@ -1,7 +1,9 @@ defmodule BlockScoutWeb.AddressTokenController do use BlockScoutWeb, :controller - import BlockScoutWeb.Chain, only: [next_page_params: 4, paging_options: 1, split_list_by_page: 1] + import BlockScoutWeb.Chain, + only: [next_page_params: 4, paging_options: 1, split_list_by_page: 1, paging_params_with_fiat_value: 1] + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] @@ -22,7 +24,7 @@ defmodule BlockScoutWeb.AddressTokenController do {tokens, next_page} = split_list_by_page(token_balances_plus_one) next_page_path = - case next_page_params(next_page, tokens, params, true) do + case next_page_params(next_page, tokens, params, &paging_params_with_fiat_value/1) do nil -> nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex index 25bec31b7dd4..94f87de09348 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do alias BlockScoutWeb.{AccessHelper, Controller, TransactionView} alias Explorer.{Chain, Market} - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, DenormalizationHelper} alias Indexer.Fetcher.CoinBalanceOnDemand alias Phoenix.View @@ -26,8 +26,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do [token_transfers: :token] => :optional, [token_transfers: :to_address] => :optional, [token_transfers: :from_address] => :optional, - [token_transfers: :token_contract_address] => :optional, - :block => :required + [token_transfers: :token_contract_address] => :optional } ] @@ -141,6 +140,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do options = @transaction_necessity_by_association + |> DenormalizationHelper.extend_block_necessity(:required) |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 639e5cc39466..a27e3153f118 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.AddressTransactionController do alias BlockScoutWeb.{AccessHelper, Controller, TransactionView} alias Explorer.{Chain, Market} + alias Explorer.Chain.Address alias Explorer.Chain.CSVExport.{ AddressInternalTransactionCsvExporter, @@ -20,7 +21,7 @@ defmodule BlockScoutWeb.AddressTransactionController do AddressTransactionCsvExporter } - alias Explorer.Chain.Wei + alias Explorer.Chain.{DenormalizationHelper, Transaction, Wei} alias Indexer.Fetcher.CoinBalanceOnDemand alias Phoenix.View @@ -32,7 +33,6 @@ defmodule BlockScoutWeb.AddressTransactionController do [created_contract_address: :names] => :optional, [from_address: :names] => :optional, [to_address: :names] => :optional, - :block => :optional, [created_contract_address: :smart_contract] => :optional, [from_address: :smart_contract] => :optional, [to_address: :smart_contract] => :optional @@ -50,10 +50,11 @@ defmodule BlockScoutWeb.AddressTransactionController do {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do options = @transaction_necessity_by_association + |> DenormalizationHelper.extend_block_necessity(:optional) |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) - results_plus_one = Chain.address_to_transactions_with_rewards(address_hash, options) + results_plus_one = Transaction.address_to_transactions_with_rewards(address_hash, options) {results, next_page} = split_list_by_page(results_plus_one) next_page_url = @@ -190,7 +191,7 @@ defmodule BlockScoutWeb.AddressTransactionController do ) when is_binary(address_hash_string) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:address_exists, true} <- {:address_exists, Chain.address_exists?(address_hash)}, + {:address_exists, true} <- {:address_exists, Address.address_exists?(address_hash)}, {:recaptcha, true} <- {:recaptcha, captcha_helper().recaptcha_passed?(recaptcha_response)} do filter_type = Map.get(params, "filter_type") filter_value = Map.get(params, "filter_value") @@ -229,7 +230,7 @@ defmodule BlockScoutWeb.AddressTransactionController do ) when is_binary(address_hash_string) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:address_exists, true} <- {:address_exists, Chain.address_exists?(address_hash)}, + {:address_exists, true} <- {:address_exists, Address.address_exists?(address_hash)}, true <- Application.get_env(:block_scout_web, :recaptcha)[:is_disabled] do filter_type = Map.get(params, "filter_type") filter_value = Map.get(params, "filter_value") diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex index 2a4287cf98d6..f67ef4afb9dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex @@ -3,20 +3,29 @@ defmodule BlockScoutWeb.API.EthRPC.EthController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView + alias BlockScoutWeb.API.RPC.RPCView alias Explorer.EthRPC def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do - case AccessHelper.check_rate_limit(conn) do - :ok -> - responses = EthRPC.responses(requests) + eth_json_rpc_max_batch_size = Application.get_env(:block_scout_web, :api_rate_limit)[:eth_json_rpc_max_batch_size] - conn - |> put_status(200) - |> put_view(EthRPCView) - |> render("responses.json", %{responses: responses}) + with :ok <- AccessHelper.check_rate_limit(conn), + {:batch_size, true} <- {:batch_size, Enum.count(requests) <= eth_json_rpc_max_batch_size} do + responses = EthRPC.responses(requests) + conn + |> put_status(200) + |> put_view(EthRPCView) + |> render("responses.json", %{responses: responses}) + else :rate_limit_reached -> AccessHelper.handle_rate_limit_deny(conn) + + {:batch_size, _} -> + conn + |> put_status(413) + |> put_view(RPCView) + |> render(:error, %{:error => "Payload Too Large. Max batch size is #{eth_json_rpc_max_batch_size}"}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index e3d33c1419ab..140dea3512c6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -7,6 +7,12 @@ defmodule BlockScoutWeb.API.RPC.AddressController do alias Explorer.Etherscan.{Addresses, Blocks} alias Indexer.Fetcher.CoinBalanceOnDemand + @api_true [api?: true] + + @invalid_address_message "Invalid address format" + @invalid_contract_address_message "Invalid contract address format" + @no_token_transfers_message "No token transfers found" + def listaccounts(conn, params) do options = params @@ -88,7 +94,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:format, :error} -> conn |> put_status(200) - |> render(:error, error: "Invalid address format") + |> render(:error, error: @invalid_address_message) {:error, :not_found} -> render(conn, :error, error: "No transactions found", data: []) @@ -100,7 +106,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param), - {:address, :ok} <- {:address, Chain.check_address_exists(address_hash)}, + {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, transactions} <- list_transactions(address_hash, options) do render(conn, :txlist, %{transactions: transactions}) else @@ -112,7 +118,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:format, :error} -> conn |> put_status(200) - |> render(:error, error: "Invalid address format") + |> render(:error, error: @invalid_address_message) {_, :not_found} -> render(conn, :error, error: "No transactions found", data: []) @@ -149,12 +155,12 @@ defmodule BlockScoutWeb.API.RPC.AddressController do options = optional_params(params) with {:format, {:ok, address_hash}} <- to_address_hash(address_param), - {:address, :ok} <- {:address, Chain.check_address_exists(address_hash)}, + {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, internal_transactions} <- list_internal_transactions(address_hash, options) do render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) else {:format, :error} -> - render(conn, :error, error: "Invalid address format") + render(conn, :error, error: @invalid_address_message) {_, :not_found} -> render(conn, :error, error: "No internal transactions found", data: []) @@ -166,8 +172,9 @@ defmodule BlockScoutWeb.API.RPC.AddressController do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param), - {:contract_address, {:ok, contract_address_hash}} <- to_contract_address_hash(params["contractaddress"]), - {:address, :ok} <- {:address, Chain.check_address_exists(address_hash)}, + {:contract_address, {:ok, contract_address_hash}} <- + {:contract_address, to_address_hash_optional(params["contractaddress"])}, + {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, token_transfers} <- list_token_transfers(address_hash, contract_address_hash, options) do render(conn, :tokentx, %{token_transfers: token_transfers}) else @@ -175,13 +182,38 @@ defmodule BlockScoutWeb.API.RPC.AddressController do render(conn, :error, error: "Query parameter address is required") {:format, :error} -> - render(conn, :error, error: "Invalid address format") + render(conn, :error, error: @invalid_address_message) + + {:contract_address, :error} -> + render(conn, :error, error: @invalid_contract_address_message) + + {_, :not_found} -> + render(conn, :error, error: @no_token_transfers_message, data: []) + end + end + + def tokennfttx(conn, params) do + options = optional_params(params) + + with {:address, {:ok, address_hash}} <- {:address, to_address_hash_optional(params["address"])}, + {:contract_address, {:ok, contract_address_hash}} <- + {:contract_address, to_address_hash_optional(params["contractaddress"])}, + true <- !is_nil(address_hash) or !is_nil(contract_address_hash), + {:ok, token_transfers, max_block_number} <- + list_nft_transfers(address_hash, contract_address_hash, options) do + render(conn, :tokennfttx, %{token_transfers: token_transfers, max_block_number: max_block_number}) + else + false -> + render(conn, :error, error: "Query parameter address or contractaddress is required") + + {:address, :error} -> + render(conn, :error, error: @invalid_address_message) {:contract_address, :error} -> - render(conn, :error, error: "Invalid contract address format") + render(conn, :error, error: @invalid_contract_address_message) {_, :not_found} -> - render(conn, :error, error: "No token transfers found", data: []) + render(conn, :error, error: @no_token_transfers_message, data: []) end end @@ -205,7 +237,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do def tokenlist(conn, params) do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param), - {:address, :ok} <- {:address, Chain.check_address_exists(address_hash)}, + {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, token_list} <- list_tokens(address_hash) do render(conn, :token_list, %{token_list: token_list}) else @@ -213,7 +245,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do render(conn, :error, error: "Query parameter address is required") {:format, :error} -> - render(conn, :error, error: "Invalid address format") + render(conn, :error, error: @invalid_address_message) {_, :not_found} -> render(conn, :error, error: "No tokens found", data: []) @@ -225,7 +257,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param), - {:address, :ok} <- {:address, Chain.check_address_exists(address_hash)}, + {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, blocks} <- list_blocks(address_hash, options) do render(conn, :getminedblocks, %{blocks: blocks}) else @@ -233,7 +265,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do render(conn, :error, error: "Query parameter 'address' is required") {:format, :error} -> - render(conn, :error, error: "Invalid address format") + render(conn, :error, error: @invalid_address_message) {_, :not_found} -> render(conn, :error, error: "No blocks found", data: []) @@ -249,8 +281,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do %{} |> put_order_by_direction(params) |> Helper.put_pagination_options(params) - |> put_block(params, "start_block") - |> put_block(params, "end_block") + |> put_block(params, "startblock") + |> put_block(params, "endblock") |> put_filter_by(params) |> put_timestamp(params, "start_timestamp") |> put_timestamp(params, "end_timestamp") @@ -395,11 +427,9 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end) end - defp to_contract_address_hash(nil), do: {:contract_address, {:ok, nil}} + defp to_address_hash_optional(nil), do: {:ok, nil} - defp to_contract_address_hash(address_hash_string) do - {:contract_address, Chain.string_to_address_hash(address_hash_string)} - end + defp to_address_hash_optional(address_hash_string), do: Chain.string_to_address_hash(address_hash_string) defp to_address_hash(address_hash_string) do {:format, Chain.string_to_address_hash(address_hash_string)} @@ -501,6 +531,29 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end + defp list_nft_transfers(nil, contract_address_hash, options) do + with {:ok, max_block_number} <- Chain.max_consensus_block_number(), + token_transfers when token_transfers != [] <- + Etherscan.list_nft_transfers_by_token(contract_address_hash, options) do + {:ok, token_transfers, max_block_number} + else + _ -> + {:error, :not_found} + end + end + + defp list_nft_transfers(address_hash, contract_address_hash, options) do + with {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, + {:ok, max_block_number} <- Chain.max_consensus_block_number(), + token_transfers when token_transfers != [] <- + Etherscan.list_nft_transfers(address_hash, contract_address_hash, options) do + {:ok, token_transfers, max_block_number} + else + _ -> + {:error, :not_found} + end + end + defp list_blocks(address_hash, options) do case Etherscan.list_blocks(address_hash, options) do [] -> {:error, :not_found} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex index 68256be97564..f5b87aea0b68 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex @@ -4,14 +4,40 @@ defmodule BlockScoutWeb.API.RPC.BlockController do alias BlockScoutWeb.Chain, as: ChainWeb alias Explorer.Chain alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Counters.AverageBlockTime + alias Timex.Duration + @doc """ + Calculates the total reward for mining a specific block. + + ## Parameters + - conn: Plug.Conn struct. + - params: A map containing the query parameters which should include: + - `blockno`: The number of the block for which to calculate the reward. + + ## Description + This function computes the block reward, which consists of: + - The sum of the transaction fees (gas_used * gas_price) for the block. + - A static reward for the miner, which may vary over the blockchain's lifespan. + - The reward for uncle blocks calculated as (1/32 * static_reward * number_of_uncles). + + ## Responses + - On success: Renders a JSON response with the reward details for the block. + - On failure: Renders an error response with an appropriate message due to: + - Absence of the `blockno` parameter. + - Invalid `blockno` parameter. + - Non-existence of the specified block. + """ + @spec getblockreward(Plug.Conn.t(), map()) :: Plug.Conn.t() def getblockreward(conn, params) do with {:block_param, {:ok, unsafe_block_number}} <- {:block_param, Map.fetch(params, "blockno")}, {:ok, block_number} <- ChainWeb.param_to_block_number(unsafe_block_number), - {:ok, block} <- Chain.number_to_block(block_number) do - reward = Chain.block_reward(block_number) - - render(conn, :block_reward, block: block, reward: reward) + {:ok, block} <- + Chain.number_to_block(block_number, + necessity_by_association: %{rewards: :optional}, + api?: true + ) do + render(conn, :block_reward, block: block) else {:block_param, :error} -> render(conn, :error, error: "Query parameter 'blockno' is required") @@ -24,6 +50,84 @@ defmodule BlockScoutWeb.API.RPC.BlockController do end end + @doc """ + Calculates and renders the estimated time until a target block number is reached. + + ## Parameters + - conn: Plug.Conn struct. + - params: A map containing the query parameters which should include: + - `blockno`: The target block number to countdown to. + + ## Description + This function takes a target block number from the `params` map and calculates the remaining time in seconds until that block is reached, considering the current maximum block number and the average block time. + + ## Responses + - On success: Renders a view with the countdown information including the current block number, target block number, the number of remaining blocks, and the estimated time in seconds until the target block number is reached. + - On failure: Renders an error view with an appropriate message, which could be due to: + - Missing `blockno` parameter. + - Invalid block number provided. + - Average block time calculation being disabled. + - Chain is currently indexing and cannot provide the information. + - The target block number has already been passed. + """ + @spec getblockcountdown(Plug.Conn.t(), map()) :: Plug.Conn.t() + def getblockcountdown(conn, params) do + with {:block_param, {:ok, unsafe_target_block_number}} <- {:block_param, Map.fetch(params, "blockno")}, + {:ok, target_block_number} <- ChainWeb.param_to_block_number(unsafe_target_block_number), + {:max_block, current_block_number} when not is_nil(current_block_number) <- + {:max_block, BlockNumber.get_max()}, + {:average_block_time, average_block_time} when is_struct(average_block_time) <- + {:average_block_time, AverageBlockTime.average_block_time()}, + {:remaining_blocks, remaining_blocks} when remaining_blocks > 0 <- + {:remaining_blocks, target_block_number - current_block_number} do + estimated_time_in_sec = Float.round(remaining_blocks * Duration.to_seconds(average_block_time), 1) + + render(conn, :block_countdown, + current_block: current_block_number, + countdown_block: target_block_number, + remaining_blocks: remaining_blocks, + estimated_time_in_sec: estimated_time_in_sec + ) + else + {:block_param, :error} -> + render(conn, :error, error: "Query parameter 'blockno' is required") + + {:error, :invalid} -> + render(conn, :error, error: "Invalid block number") + + {:average_block_time, {:error, :disabled}} -> + render(conn, :error, error: "Average block time calculating is disabled, so getblockcountdown is not available") + + {stage, _} when stage in ~w(max_block average_block_time)a -> + render(conn, :error, error: "Chain is indexing now, try again later") + + {:remaining_blocks, _} -> + render(conn, :error, error: "Error! Block number already pass") + end + end + + @doc """ + Retrieves the block number associated with a given timestamp and closest policy. + + ## Parameters + - conn: Plug.Conn struct. + - params: A map containing the query parameters which should include: + - `timestamp`: The timestamp to query the block number for. + - `closest`: The policy to determine which block number to return. It could be a value like 'before' or 'after' to indicate whether the closest block before or after the given timestamp should be returned. + + ## Description + This function finds the block number that is closest to a specific timestamp according to the provided 'closest' policy. + + ## Responses + - On success: Renders a JSON response with the found block number. + - On failure: Renders an error response with an appropriate message, which could be due to: + - Missing `timestamp` parameter. + - Missing `closest` parameter. + - Invalid `timestamp` parameter. + - Invalid `closest` parameter. + - No block corresponding to the given timestamp and closest policy. + """ + @spec getblocknobytime(Plug.Conn.t(), map()) :: Plug.Conn.t() def getblocknobytime(conn, params) do from_api = true @@ -51,6 +155,21 @@ defmodule BlockScoutWeb.API.RPC.BlockController do end end + @doc """ + Fetches the highest block number from the chain. + + ## Parameters + - conn: Plug.Conn struct. + - params: A map containing the query parameters which may include: + - `id`: An optional parameter that defaults to 1 if not provided. + + ## Description + This function retrieves the maximum block number that has been recorded in the blockchain. + + ## Responses + - Renders a JSON response including the maximum block number and the provided or default `id`. + """ + @spec eth_block_number(Plug.Conn.t(), map()) :: Plug.Conn.t() def eth_block_number(conn, params) do id = Map.get(params, "id", 1) max_block_number = BlockNumber.get_max() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex index fd80cf7ad77d..595b6116535b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -6,6 +6,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do alias BlockScoutWeb.API.RPC.{AddressController, Helper} alias Explorer.Chain alias Explorer.Chain.{Address, Hash, SmartContract} + alias Explorer.Chain.SmartContract.Proxy.VerificationStatus, as: ProxyVerificationStatus alias Explorer.Chain.SmartContract.VerificationStatus alias Explorer.Etherscan.Contracts alias Explorer.SmartContract.Helper, as: SmartContractHelper @@ -13,12 +14,49 @@ defmodule BlockScoutWeb.API.RPC.ContractController do alias Explorer.SmartContract.Solidity.PublisherWorker, as: SolidityPublisherWorker alias Explorer.SmartContract.Vyper.Publisher, as: VyperPublisher alias Explorer.ThirdPartyIntegrations.Sourcify + import BlockScoutWeb.API.V2.AddressController, only: [validate_address: 2, validate_address: 3] @smth_went_wrong "Something went wrong while publishing the contract" @verified "Smart-contract already verified." @invalid_address "Invalid address hash" @invalid_args "Invalid args format" @address_required "Query parameter address is required" + @addresses_required "Query parameter contractaddresses is required" + @contract_not_found "Smart-contract not found or is not verified" + @restricted_access "Access to this address is restricted" + + @addresses_limit 10 + @api_true [api?: true] + + @doc """ + Function to handle getcontractcreation request + """ + @spec getcontractcreation(Plug.Conn.t(), map()) :: Plug.Conn.t() + def getcontractcreation(conn, %{"contractaddresses" => contract_address_hash_strings} = params) do + addresses = + contract_address_hash_strings + |> String.split(",") + |> Enum.take(@addresses_limit) + |> Enum.map(fn address_hash_string -> + case validate_address(address_hash_string, params) do + {:ok, _address_hash, address} -> + Address.maybe_preload_smart_contract_associations( + address, + [:contracts_creation_internal_transaction, :contracts_creation_transaction], + @api_true + ) + + _ -> + nil + end + end) + + render(conn, :getcontractcreation, %{addresses: addresses}) + end + + def getcontractcreation(conn, _params) do + render(conn, :error, error: @addresses_required, data: @addresses_required) + end def verify(conn, %{"addressHash" => address_hash} = params) do with {:params, {:ok, fetched_params}} <- {:params, fetch_verify_params(params)}, @@ -83,7 +121,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do [] end - if Chain.smart_contract_fully_verified?(address_hash) do + if SmartContract.verified_with_full_match?(address_hash) do render(conn, :error, error: @verified) else case Sourcify.check_by_address(address_hash) do @@ -114,7 +152,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do } = params ) do with {:check_verified_status, false} <- - {:check_verified_status, Chain.smart_contract_fully_verified?(address_hash)}, + {:check_verified_status, SmartContract.verified_with_full_match?(address_hash)}, {:format, {:ok, _casted_address_hash}} <- to_address_hash(address_hash), {:params, {:ok, fetched_params}} <- {:params, fetch_verifysourcecode_params(params)}, uid <- VerificationStatus.generate_uid(address_hash) do @@ -137,6 +175,38 @@ defmodule BlockScoutWeb.API.RPC.ContractController do render(conn, :error, error: "Missing sourceCode or contractaddress fields") end + def verifysourcecode( + conn, + %{ + "codeformat" => "solidity-single-file", + "contractaddress" => address_hash + } = params + ) do + with {:check_verified_status, false} <- + {:check_verified_status, SmartContract.verified_with_full_match?(address_hash)}, + {:format, {:ok, _casted_address_hash}} <- to_address_hash(address_hash), + {:params, {:ok, fetched_params}} <- {:params, fetch_verifysourcecode_solidity_single_file_params(params)}, + external_libraries <- fetch_external_libraries_for_verifysourcecode(params), + uid <- VerificationStatus.generate_uid(address_hash) do + Que.add(SolidityPublisherWorker, {"flattened_api", fetched_params, external_libraries, uid}) + + render(conn, :show, %{result: uid}) + else + {:check_verified_status, true} -> + render(conn, :error, error: @verified, data: @verified) + + {:format, :error} -> + render(conn, :error, error: @invalid_address, data: @invalid_address) + + {:params, {:error, error}} -> + render(conn, :error, error: error, data: error) + end + end + + def verifysourcecode(conn, _params) do + render(conn, :error, error: "Missing codeformat field") + end + def checkverifystatus(conn, %{"guid" => guid}) do case VerificationStatus.fetch_status(guid) do :pending -> @@ -153,6 +223,65 @@ defmodule BlockScoutWeb.API.RPC.ContractController do end end + def verifyproxycontract(conn, %{"address" => address_hash_string} = params) do + with {:ok, address_hash, %Address{smart_contract: smart_contract}} <- + validate_address(address_hash_string, params, + necessity_by_association: %{:smart_contract => :optional}, + api?: true + ), + {:not_found, false} <- {:not_found, is_nil(smart_contract)}, + {:time_interval, true} <- + {:time_interval, + SmartContract.check_implementation_refetch_necessity(smart_contract.implementation_fetched_at)}, + uid <- ProxyVerificationStatus.generate_uid(address_hash) do + ProxyVerificationStatus.insert_status(uid, :pending, address_hash) + + SmartContract.get_implementation_address_hash(smart_contract, + timeout: 0, + uid: uid, + callback: &ProxyVerificationStatus.set_proxy_verification_result/2 + ) + + render(conn, :show, %{result: uid}) + else + {:format, :error} -> + render(conn, :error, error: @invalid_address) + + {:not_found, _} -> + render(conn, :error, error: @contract_not_found) + + {:restricted_access, true} -> + render(conn, :error, error: @restricted_access) + + {:time_interval, false} -> + render(conn, :error, error: "Only one attempt in #{SmartContract.get_fresh_time_distance()}ms") + end + end + + def checkproxyverification(conn, %{"guid" => guid}) do + submission = ProxyVerificationStatus.fetch_status(guid) + + case submission && submission.status do + :pending -> + render(conn, :show, %{result: "Verification in progress"}) + + :pass -> + render(conn, :show, %{ + result: + "The proxy's (#{submission.contract_address_hash}) implementation contract is found at #{SmartContract.address_hash_to_smart_contract(submission.contract_address_hash).implementation_address_hash} and is successfully updated." + }) + + :fail -> + render(conn, :error, %{ + error: "NOTOK", + data: "A corresponding implementation contract was unfortunately not detected for the proxy address." + }) + + _ -> + render(conn, :show, %{result: "Unknown UID"}) + end + end + defp prepare_params(files) when is_struct(files) do {:error, @invalid_args} end @@ -352,7 +481,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param) do _ = PublishHelper.check_and_verify(address_param) - address = Contracts.address_hash_to_address_with_source_code(address_hash) + address = Contracts.address_hash_to_address_with_source_code(address_hash, false) render(conn, :getsourcecode, %{ contract: address || %Address{hash: address_hash, smart_contract: nil} @@ -457,7 +586,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do _ = PublishHelper.check_and_verify(Hash.to_string(address_hash)) result = - case Chain.address_hash_to_smart_contract(address_hash) do + case SmartContract.address_hash_to_smart_contract(address_hash) do nil -> :not_found @@ -497,7 +626,21 @@ defmodule BlockScoutWeb.API.RPC.ContractController do |> required_param(params, "contractname", "name") |> required_param(params, "compilerversion", "compiler_version") |> optional_param(params, "constructorArguments", "constructor_arguments") + |> optional_param(params, "licenseType", "license_type") + end + + defp fetch_verifysourcecode_solidity_single_file_params(params) do + {:ok, %{}} + |> required_param(params, "contractaddress", "address_hash") + |> required_param(params, "contractname", "name") + |> required_param(params, "compilerversion", "compiler_version") + |> required_param(params, "optimizationUsed", "optimization") + |> required_param(params, "sourceCode", "contract_source_code") + |> optional_param(params, "runs", "optimization_runs") + |> optional_param(params, "evmversion", "evm_version") |> optional_param(params, "constructorArguments", "constructor_arguments") + |> optional_param(params, "licenseType", "license_type") + |> prepare_optimization() end defp parse_optimization_runs({:ok, %{"optimization_runs" => runs} = opts}) when is_bitstring(runs) do @@ -521,10 +664,18 @@ defmodule BlockScoutWeb.API.RPC.ContractController do defp parse_optimization_runs(other), do: other defp fetch_external_libraries(params) do + fetch_external_libraries_general(&"library#{&1}Name", &"library#{&1}Address", params) + end + + defp fetch_external_libraries_for_verifysourcecode(params) do + fetch_external_libraries_general(&"libraryname#{&1}", &"libraryaddress#{&1}", params) + end + + defp fetch_external_libraries_general(number_to_library_name, number_to_library_address, params) do Enum.reduce(1..Application.get_env(:block_scout_web, :contract)[:verification_max_libraries], %{}, fn number, acc -> - case Map.fetch(params, "library#{number}Name") do + case Map.fetch(params, number_to_library_name.(number)) do {:ok, library_name} -> - library_address = Map.get(params, "library#{number}Address") + library_address = Map.get(params, number_to_library_address.(number)) acc |> Map.put("library#{number}_name", library_name) @@ -559,4 +710,32 @@ defmodule BlockScoutWeb.API.RPC.ContractController do {:ok, map} end end + + defp prepare_optimization({:ok, %{"optimization" => optimization} = params}) do + parsed = parse_optimization(optimization) + + case parsed do + :error -> + {:error, "optimizationUsed has invalid format"} + + _ -> + {:ok, Map.put(params, "optimization", parsed)} + end + end + + defp prepare_optimization(error), do: error + + defp parse_optimization("0"), do: false + defp parse_optimization(0), do: false + + defp parse_optimization("1"), do: true + defp parse_optimization(1), do: true + + defp parse_optimization("false"), do: false + defp parse_optimization(false), do: false + + defp parse_optimization("true"), do: true + defp parse_optimization(true), do: true + + defp parse_optimization(_), do: :error end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/helper.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/helper.ex index 46840eda2555..d19627e62ec5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/helper.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.RPC.Helper do @moduledoc """ Small helpers for RPC api controllers. """ - alias Explorer.Etherscan + alias Explorer.{Chain, Etherscan} def put_pagination_options(options, params) do options @@ -39,4 +39,22 @@ defmodule BlockScoutWeb.API.RPC.Helper do defp validate_max_page_size(page_size) do if page_size <= Etherscan.page_size_max(), do: :ok, else: :error end + + @doc """ + Parses addresses list (delimiter is `,`) from a string and validates them. + """ + @spec parse_and_validate_addresses(binary(), integer()) :: list() + def parse_and_validate_addresses(string, limit) do + string + |> String.split(",") + |> Enum.take(limit) + |> Enum.uniq() + |> Enum.map(fn address -> + case Chain.string_to_address_hash(address) do + {:ok, address_hash} -> address_hash + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index b27554eed39b..0263abd96da9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -29,7 +29,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do end def call(%Conn{params: %{"module" => module, "action" => action}} = conn, translations) do - with {:valid_api_request, true} <- {:valid_api_request, valid_api_request_path(conn)}, + with {:valid_api_v1_request, true} <- {:valid_api_v1_request, valid_api_v1_request_path(conn)}, {:ok, {controller, write_actions}} <- translate_module(translations, module), {:ok, action} <- translate_action(action), true <- action_accessed?(action, write_actions), @@ -44,6 +44,13 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do |> Controller.render(:error, error: "Unknown action") |> halt() + {:error, :no_module} -> + conn + |> put_status(400) + |> put_view(RPCView) + |> Controller.render(:error, error: "Unknown module") + |> halt() + {:error, error} -> APILogger.error(fn -> ["Error while calling RPC action", inspect(error, limit: :infinity, printable_limit: :infinity)] @@ -58,7 +65,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do :rate_limit_reached -> AccessHelper.handle_rate_limit_deny(conn) - {:valid_api_request, false} -> + {:valid_api_v1_request, false} -> conn |> put_status(404) |> put_view(RPCView) @@ -89,7 +96,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do case Map.fetch(translations, module_lowercase) do {:ok, module} -> {:ok, module} - _ -> {:error, :no_action} + _ -> {:error, :no_module} end end @@ -125,9 +132,10 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do {:error, Exception.format(:error, e, __STACKTRACE__)} end - defp valid_api_request_path(conn) do - if conn.request_path == "/api" || conn.request_path == "/api/" || conn.request_path == "/api/v1" || - conn.request_path == "/api/v1/" do + defp valid_api_v1_request_path(conn) do + if String.ends_with?(conn.request_path, "/api") || String.ends_with?(conn.request_path, "/api/") || + String.ends_with?(conn.request_path, "/api/v1") || + String.ends_with?(conn.request_path, "/api/v1/") do true else false diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex index 19fcf8768c64..8b766c7715b8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex @@ -1,8 +1,6 @@ defmodule BlockScoutWeb.API.RPC.StatsController do use BlockScoutWeb, :controller - use Explorer.Schema - alias Explorer.{Chain, Etherscan, Market} alias Explorer.Chain.Cache.{AddressSum, AddressSumMinusBurnt} alias Explorer.Chain.Wei @@ -59,6 +57,12 @@ defmodule BlockScoutWeb.API.RPC.StatsController do render(conn, "coinsupply.json", total_supply: cached_coin_total_supply) end + def ethprice(conn, _params) do + rates = Market.get_coin_exchange_rate() + + render(conn, "ethprice.json", rates: rates) + end + def coinprice(conn, _params) do rates = Market.get_coin_exchange_rate() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex index 4f6ccd5a448d..51e1aff18221 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.RPC.TokenController do alias BlockScoutWeb.API.RPC.Helper alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain.BridgedToken def gettoken(conn, params) do with {:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params), @@ -50,6 +51,38 @@ defmodule BlockScoutWeb.API.RPC.TokenController do end end + if Application.compile_env(:explorer, BridgedToken)[:enabled] do + @api_true [api?: true] + def bridgedtokenlist(conn, params) do + import BlockScoutWeb.PagingHelper, + only: [ + chain_ids_filter_options: 1, + tokens_sorting: 1 + ] + + import BlockScoutWeb.Chain, + only: [ + paging_options: 1 + ] + + bridged_tokens = + if BridgedToken.enabled?() do + options = + params + |> paging_options() + |> Keyword.merge(chain_ids_filter_options(params)) + |> Keyword.merge(tokens_sorting(params)) + |> Keyword.merge(@api_true) + + "" |> BridgedToken.list_top_bridged_tokens(options) + else + [] + end + + render(conn, "bridgedtokenlist.json", %{bridged_tokens: bridged_tokens}) + end + end + defp fetch_contractaddress(params) do {:contractaddress_param, Map.fetch(params, "contractaddress")} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex index e14aef4b94e4..fbea15ca76f1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] alias Explorer.Chain - alias Explorer.Chain.Transaction + alias Explorer.Chain.{DenormalizationHelper, Transaction} @api_true [api?: true] @@ -75,7 +75,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do end defp transaction_from_hash(transaction_hash) do - case Chain.hash_to_transaction(transaction_hash, necessity_by_association: %{block: :required}) do + case Chain.hash_to_transaction(transaction_hash, DenormalizationHelper.extend_block_necessity([], :required)) do {:error, :not_found} -> {:transaction, :error} {:ok, transaction} -> {:transaction, {:ok, transaction}} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex index cb16c6876961..bde1d1a54f88 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/decompiled_smart_contract_controller.ex @@ -2,12 +2,13 @@ defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do use BlockScoutWeb, :controller alias Explorer.Chain - alias Explorer.Chain.Hash.Address + alias Explorer.Chain.Address + alias Explorer.Chain.Hash.Address, as: AddressHash def create(conn, params) do if auth_token(conn) == actual_token() do with {:ok, hash} <- validate_address_hash(params["address_hash"]), - :ok <- Chain.check_address_exists(hash), + :ok <- Address.check_address_exists(hash), {:contract, :not_found} <- {:contract, Chain.check_decompiled_contract_exists(params["address_hash"], params["decompiler_version"])} do case Chain.create_decompiled_smart_contract(params) do @@ -46,7 +47,7 @@ defmodule BlockScoutWeb.API.V1.DecompiledSmartContractController do end defp validate_address_hash(address_hash) do - case Address.cast(address_hash) do + case AddressHash.cast(address_hash) do {:ok, hash} -> {:ok, hash} :error -> :invalid_address end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex index 7b70b4b4276f..1c99dcb17136 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex @@ -30,8 +30,8 @@ defmodule BlockScoutWeb.API.V1.GasPriceOracleController do |> send_resp(status, result) end - def result(gas_prices) do - gas_prices + defp result(gas_prices) do + %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]} |> Jason.encode!() end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex index 6608c246d062..6a93491f21eb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex @@ -1,14 +1,14 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do use BlockScoutWeb, :controller - alias Explorer.Chain - alias Explorer.Chain.Hash.Address + alias Explorer.Chain.Hash.Address, as: AddressHash + alias Explorer.Chain.{Address, SmartContract} alias Explorer.SmartContract.Solidity.Publisher def create(conn, params) do with {:ok, hash} <- validate_address_hash(params["address_hash"]), - :ok <- Chain.check_address_exists(hash), - {:contract, :not_found} <- {:contract, Chain.check_verified_smart_contract_exists(hash)} do + :ok <- Address.check_address_exists(hash), + {:contract, :not_found} <- {:contract, SmartContract.check_verified_smart_contract_exists(hash)} do external_libraries = fetch_external_libraries(params) case Publisher.publish(hash, params, external_libraries) do @@ -41,7 +41,7 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do end defp validate_address_hash(address_hash) do - case Address.cast(address_hash) do + case AddressHash.cast(address_hash) do {:ok, hash} -> {:ok, hash} :error -> :invalid_address end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 5512f086f133..509e395dd1de 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -8,16 +8,26 @@ defmodule BlockScoutWeb.API.V2.AddressController do token_transfers_next_page_params: 3, paging_options: 1, split_list_by_page: 1, - current_filter: 1 + current_filter: 1, + paging_params_with_fiat_value: 1 ] import BlockScoutWeb.PagingHelper, - only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1] + only: [ + delete_parameters_from_next_page_params: 1, + token_transfers_types_options: 1, + address_transactions_sorting: 1, + nft_types_options: 1 + ] + + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1, maybe_preload_ens_to_address: 1] alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} alias Explorer.{Chain, Market} + alias Explorer.Chain.{Address, Hash, Transaction} alias Explorer.Chain.Address.Counters + alias Explorer.Chain.Token.Instance alias Indexer.Fetcher.{CoinBalanceOnDemand, TokenBalanceOnDemand} @transaction_necessity_by_association [ @@ -38,42 +48,50 @@ defmodule BlockScoutWeb.API.V2.AddressController do :to_address => :optional, :from_address => :optional, :block => :optional, - :transaction => :optional + :transaction => :optional, + :token => :optional }, api?: true ] @address_options [ necessity_by_association: %{ - :contracts_creation_internal_transaction => :optional, :names => :optional, - :smart_contract => :optional, - :token => :optional, - :contracts_creation_transaction => :optional + :token => :optional }, api?: true ] + @contract_address_preloads [ + :smart_contract, + :contracts_creation_internal_transaction, + :contracts_creation_transaction + ] + + @nft_necessity_by_association [ + necessity_by_association: %{ + :token => :optional + } + ] + @api_true [api?: true] action_fallback(BlockScoutWeb.API.V2.FallbackController) def address(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash, @address_options)} do - CoinBalanceOnDemand.trigger_fetch(address) + with {:ok, _address_hash, address} <- validate_address(address_hash_string, params, @address_options), + fully_preloaded_address <- + Address.maybe_preload_smart_contract_associations(address, @contract_address_preloads, @api_true) do + CoinBalanceOnDemand.trigger_fetch(fully_preloaded_address) conn |> put_status(200) - |> render(:address, %{address: address}) + |> render(:address, %{address: fully_preloaded_address |> maybe_preload_ens_to_address()}) end end def counters(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, _address_hash, address} <- validate_address(address_hash_string, params) do {validation_count} = Counters.address_counters(address, @api_true) transactions_from_db = address.transactions_count || 0 @@ -90,9 +108,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def token_balances(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do token_balances = address_hash |> Chain.fetch_last_token_balances(@api_true) @@ -108,23 +124,28 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def transactions(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do options = @transaction_necessity_by_association |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) + |> Keyword.merge(address_transactions_sorting(params)) - results_plus_one = Chain.address_to_transactions_without_rewards(address_hash, options, false) + results_plus_one = Transaction.address_to_transactions_without_rewards(address_hash, options, false) {transactions, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + next_page_params = + next_page + |> next_page_params( + transactions, + delete_parameters_from_next_page_params(params), + &Transaction.address_transactions_next_page_params/1 + ) conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) + |> render(:transactions, %{transactions: transactions |> maybe_preload_ens(), next_page_params: next_page_params}) end end @@ -132,12 +153,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn, %{"address_hash_param" => address_hash_string, "token" => token_address_hash_string} = params ) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:format, {:ok, token_address_hash}} <- {:format, Chain.string_to_address_hash(token_address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:ok, false} <- AccessHelper.restricted_access?(token_address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)}, - {:not_found, {:ok, _}} <- {:not_found, Chain.token_from_address_hash(token_address_hash, @api_true)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params), + {:ok, token_address_hash, _token_address} <- validate_address(token_address_hash_string, params) do paging_options = paging_options(params) options = @@ -171,14 +188,15 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) + |> render(:token_transfers, %{ + token_transfers: token_transfers |> maybe_preload_ens(), + next_page_params: next_page_params + }) end end def token_transfers(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do paging_options = paging_options(params) options = @@ -202,14 +220,15 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) + |> render(:token_transfers, %{ + token_transfers: token_transfers |> maybe_preload_ens(), + next_page_params: next_page_params + }) end end def internal_transactions(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do full_options = [ necessity_by_association: %{ @@ -235,16 +254,14 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> put_status(200) |> put_view(TransactionView) |> render(:internal_transactions, %{ - internal_transactions: internal_transactions, + internal_transactions: internal_transactions |> maybe_preload_ens(), next_page_params: next_page_params }) end end def logs(conn, %{"address_hash_param" => address_hash_string, "topic" => topic} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do prepared_topic = String.trim(topic) formatted_topic = if String.starts_with?(prepared_topic, "0x"), do: prepared_topic, else: "0x" <> prepared_topic @@ -260,14 +277,12 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:logs, %{logs: logs, next_page_params: next_page_params}) + |> render(:logs, %{logs: logs |> maybe_preload_ens(), next_page_params: next_page_params}) end end def logs(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do options = params |> paging_options() |> Keyword.merge(@api_true) results_plus_one = Chain.address_to_logs(address_hash, false, options) @@ -279,14 +294,12 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:logs, %{logs: logs, next_page_params: next_page_params}) + |> render(:logs, %{logs: logs |> maybe_preload_ens(), next_page_params: next_page_params}) end end def blocks_validated(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do full_options = [ necessity_by_association: %{ @@ -330,9 +343,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def coin_balance_history_by_day(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do balances_by_day = address_hash |> Chain.address_to_balances_by_day(@api_true) @@ -344,9 +355,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def tokens(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do results_plus_one = address_hash |> Chain.fetch_paginated_last_token_balances( @@ -362,7 +371,13 @@ defmodule BlockScoutWeb.API.V2.AddressController do {tokens, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(tokens, delete_parameters_from_next_page_params(params), true) + next_page_params = + next_page + |> next_page_params( + tokens, + delete_parameters_from_next_page_params(params), + &paging_params_with_fiat_value/1 + ) conn |> put_status(200) @@ -371,9 +386,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def withdrawals(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do options = @api_true |> Keyword.merge(paging_options(params)) withdrawals_plus_one = address_hash |> Chain.address_hash_to_withdrawals(options) {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) @@ -383,7 +396,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) |> put_view(WithdrawalView) - |> render(:withdrawals, %{withdrawals: withdrawals, next_page_params: next_page_params}) + |> render(:withdrawals, %{withdrawals: withdrawals |> maybe_preload_ens(), next_page_params: next_page_params}) end end @@ -392,7 +405,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do params |> paging_options() |> Keyword.merge(@api_true) - |> Chain.list_top_addresses() + |> Address.list_top_addresses() |> split_list_by_page() next_page_params = next_page_params(next_page, addresses, params) @@ -403,7 +416,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) |> render(:addresses, %{ - addresses: addresses, + addresses: addresses |> maybe_preload_ens(), next_page_params: next_page_params, exchange_rate: exchange_rate, total_supply: total_supply @@ -411,9 +424,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def tabs_counters(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do {validations, transactions, token_transfers, token_balances, logs, withdrawals, internal_txs} = Counters.address_limited_counters(address_hash, @api_true) @@ -430,4 +441,77 @@ defmodule BlockScoutWeb.API.V2.AddressController do }) end end + + def nft_list(conn, %{"address_hash_param" => address_hash_string} = params) do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do + results_plus_one = + Instance.nft_list( + address_hash, + params + |> paging_options() + |> Keyword.merge(nft_types_options(params)) + |> Keyword.merge(@api_true) + |> Keyword.merge(@nft_necessity_by_association) + ) + + {nfts, next_page} = split_list_by_page(results_plus_one) + + next_page_params = + next_page + |> next_page_params( + nfts, + delete_parameters_from_next_page_params(params), + &Instance.nft_list_next_page_params/1 + ) + + conn + |> put_status(200) + |> render(:nft_list, %{token_instances: nfts, next_page_params: next_page_params}) + end + end + + def nft_collections(conn, %{"address_hash_param" => address_hash_string} = params) do + with {:ok, address_hash, _address} <- validate_address(address_hash_string, params) do + results_plus_one = + Instance.nft_collections( + address_hash, + params + |> paging_options() + |> Keyword.merge(nft_types_options(params)) + |> Keyword.merge(@api_true) + |> Keyword.merge(@nft_necessity_by_association) + ) + + {collections, next_page} = split_list_by_page(results_plus_one) + + next_page_params = + next_page + |> next_page_params( + collections, + delete_parameters_from_next_page_params(params), + &Instance.nft_collections_next_page_params/1 + ) + + conn + |> put_status(200) + |> render(:nft_collections, %{collections: collections, next_page_params: next_page_params}) + end + end + + @doc """ + Checks if this valid address hash string, and this address is not prohibited address. + Returns the `{:ok, address_hash, address}` if address hash passed all the checks. + """ + @spec validate_address(String.t(), any(), Keyword.t()) :: + {:format, :error} + | {:not_found, {:error, :not_found}} + | {:restricted_access, true} + | {:ok, Hash.t(), Address.t()} + def validate_address(address_hash_string, params, options \\ @api_true) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash, options, false)} do + {:ok, address_hash, address} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex new file mode 100644 index 000000000000..f72a6424a7c3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex @@ -0,0 +1,32 @@ +defmodule BlockScoutWeb.API.V2.BlobController do + use BlockScoutWeb, :controller + + alias Explorer.Chain + alias Explorer.Chain.Beacon.{Blob, Reader} + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @doc """ + Function to handle GET requests to `/api/v2/blobs/:blob_hash_param` endpoint. + """ + @spec blob(Plug.Conn.t(), map()) :: Plug.Conn.t() + def blob(conn, %{"blob_hash_param" => blob_hash_string} = _params) do + with {:format, {:ok, blob_hash}} <- {:format, Chain.string_to_transaction_hash(blob_hash_string)} do + transaction_hashes = Reader.blob_hash_to_transactions(blob_hash, api?: true) + + {status, blob} = + case Reader.blob(blob_hash, true, api?: true) do + {:ok, blob} -> {:ok, blob} + {:error, :not_found} -> {:pending, %Blob{hash: blob_hash}} + end + + if Enum.empty?(transaction_hashes) and status == :pending do + {:error, :not_found} + else + conn + |> put_status(200) + |> render(:blob, %{blob: blob, transaction_hashes: transaction_hashes}) + end + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index a680d7616d40..6f4c51082d94 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -10,34 +10,78 @@ defmodule BlockScoutWeb.API.V2.BlockController do parse_block_hash_or_number_param: 1 ] - import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1, select_block_type: 1] + import BlockScoutWeb.PagingHelper, + only: [delete_parameters_from_next_page_params: 1, select_block_type: 1, type_filter_options: 1] + + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] alias BlockScoutWeb.API.V2.{TransactionView, WithdrawalView} alias Explorer.Chain + case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> + @chain_type_transaction_necessity_by_association %{ + :beacon_blob_transaction => :optional + } + @chain_type_block_necessity_by_association %{ + [transactions: :beacon_blob_transaction] => :optional + } + + "zksync" -> + @chain_type_transaction_necessity_by_association %{} + @chain_type_block_necessity_by_association %{ + :zksync_batch => :optional, + :zksync_commit_transaction => :optional, + :zksync_prove_transaction => :optional, + :zksync_execute_transaction => :optional + } + + _ -> + @chain_type_transaction_necessity_by_association %{} + @chain_type_block_necessity_by_association %{} + end + @transaction_necessity_by_association [ - necessity_by_association: %{ - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - :block => :optional, - [created_contract_address: :smart_contract] => :optional, - [from_address: :smart_contract] => :optional, - [to_address: :smart_contract] => :optional - } + necessity_by_association: + %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + :block => :optional, + [created_contract_address: :smart_contract] => :optional, + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional + } + |> Map.merge(@chain_type_transaction_necessity_by_association) ] @api_true [api?: true] @block_params [ - necessity_by_association: %{ - [miner: :names] => :optional, - :uncles => :optional, - :nephews => :optional, - :rewards => :optional, - :transactions => :optional, - :withdrawals => :optional - }, + necessity_by_association: + %{ + [miner: :names] => :optional, + :uncles => :optional, + :nephews => :optional, + :rewards => :optional, + :transactions => :optional, + :withdrawals => :optional + } + |> Map.merge(@chain_type_block_necessity_by_association), + api?: true + ] + + @block_params [ + necessity_by_association: + %{ + [miner: :names] => :optional, + :uncles => :optional, + :nephews => :optional, + :rewards => :optional, + :transactions => :optional, + :withdrawals => :optional + } + |> Map.merge(@chain_type_block_necessity_by_association), api?: true ] @@ -81,7 +125,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do conn |> put_status(200) - |> render(:blocks, %{blocks: blocks, next_page_params: next_page_params}) + |> render(:blocks, %{blocks: blocks |> maybe_preload_ens(), next_page_params: next_page_params}) end def transactions(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do @@ -90,6 +134,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do full_options = @transaction_necessity_by_association |> Keyword.merge(put_key_value_to_paging_options(paging_options(params), :is_index_in_asc_order, true)) + |> Keyword.merge(type_filter_options(params)) |> Keyword.merge(@api_true) transactions_plus_one = Chain.block_to_transactions(block.hash, full_options, false) @@ -103,7 +148,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) + |> render(:transactions, %{transactions: transactions |> maybe_preload_ens(), next_page_params: next_page_params}) end end @@ -122,7 +167,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do conn |> put_status(200) |> put_view(WithdrawalView) - |> render(:withdrawals, %{withdrawals: withdrawals, next_page_params: next_page_params}) + |> render(:withdrawals, %{withdrawals: withdrawals |> maybe_preload_ens(), next_page_params: next_page_params}) end end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 79cad2ebffed..88522f872f6d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -3,7 +3,9 @@ defmodule BlockScoutWeb.API.V2.FallbackController do require Logger + alias BlockScoutWeb.Account.Api.V1.UserView alias BlockScoutWeb.API.V2.ApiView + alias Ecto.Changeset @verification_failed "API v2 smart-contract verification failed" @invalid_parameters "Invalid parameter(s)" @@ -23,6 +25,14 @@ defmodule BlockScoutWeb.API.V2.FallbackController do @unauthorized "Unauthorized" @not_configured_api_key "API key not configured on the server" @wrong_api_key "Wrong API key" + @address_not_found "Address not found" + @address_is_not_smart_contract "Address is not smart-contract" + @vyper_smart_contract_is_not_supported "Vyper smart-contracts are not supported by SolidityScan" + @unverified_smart_contract "Smart-contract is unverified" + @empty_response "Empty response" + @tx_interpreter_service_disabled "Transaction Interpretation Service is disabled" + @disabled "API endpoint is disabled" + @service_disabled "Service is disabled" def call(conn, {:format, _params}) do Logger.error(fn -> @@ -119,6 +129,13 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> call({:not_found, nil}) end + def call(conn, {:error, %Changeset{} = changeset}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(UserView) + |> render(:changeset_errors, changeset: changeset) + end + def call(conn, {:restricted_access, true}) do Logger.error(fn -> ["#{@verification_failed}: #{@restricted_access}"] @@ -130,7 +147,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> render(:message, %{message: @restricted_access}) end - def call(conn, {:already_verified, true}) do + def call(conn, {:already_verified, _}) do Logger.error(fn -> ["#{@verification_failed}: #{@already_verified}"] end) @@ -232,4 +249,66 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> put_view(ApiView) |> render(:message, %{message: @wrong_api_key}) end + + def call(conn, {:address, {:error, :not_found}}) do + conn + |> put_status(:not_found) + |> put_view(ApiView) + |> render(:message, %{message: @address_not_found}) + end + + def call(conn, {:is_smart_contract, result}) when is_nil(result) or result == false do + conn + |> put_status(:not_found) + |> put_view(ApiView) + |> render(:message, %{message: @address_is_not_smart_contract}) + end + + def call(conn, {:is_vyper_contract, result}) when result == true do + conn + |> put_status(:not_found) + |> put_view(ApiView) + |> render(:message, %{message: @vyper_smart_contract_is_not_supported}) + end + + def call(conn, {:is_verified_smart_contract, result}) when result == false do + conn + |> put_status(:not_found) + |> put_view(ApiView) + |> render(:message, %{message: @unverified_smart_contract}) + end + + def call(conn, {:is_empty_response, true}) do + conn + |> put_status(500) + |> put_view(ApiView) + |> render(:message, %{message: @empty_response}) + end + + def call(conn, {:tx_interpreter_enabled, false}) do + conn + |> put_status(:forbidden) + |> put_view(ApiView) + |> render(:message, %{message: @tx_interpreter_service_disabled}) + end + + def call(conn, {:disabled, _}) do + conn + |> put_status(:forbidden) + |> put_view(ApiView) + |> render(:message, %{message: @disabled}) + end + + def call(conn, {:error, :disabled}) do + conn + |> put_status(501) + |> put_view(ApiView) + |> render(:message, %{message: @service_disabled}) + end + + def call(conn, {code, response}) when is_integer(code) do + conn + |> put_status(code) + |> json(response) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex index bf5ebe6f3280..d8fefb4f2b5c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex @@ -3,7 +3,11 @@ defmodule BlockScoutWeb.API.V2.ImportController do alias BlockScoutWeb.API.V2.ApiView alias Explorer.{Chain, Repo} - alias Explorer.Chain.Token + alias Explorer.Chain.{Data, SmartContract, Token} + alias Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand + alias Explorer.SmartContract.EthBytecodeDBInterface + + import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, contract_creation_input: 1] require Logger @api_true [api?: true] @@ -48,6 +52,47 @@ defmodule BlockScoutWeb.API.V2.ImportController do end end + @doc """ + Function to handle request at: + `/api/v2/import/smart-contracts/{address_hash_param}` + + Needed to try to import unverified smart contracts via eth-bytecode-db (`/api/v2/bytecodes/sources:search` method). + Protected by `x-api-key` header. + """ + @spec try_to_search_contract(Plug.Conn.t(), map()) :: + {:already_verified, nil | SmartContract.t()} + | {:api_key, nil | binary()} + | {:format, :error} + | {:not_found, {:error, :not_found}} + | {:sensitive_endpoints_api_key, any()} + | Plug.Conn.t() + def try_to_search_contract(conn, %{"address_hash_param" => address_hash_string}) do + with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- + {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, + {:api_key, ^api_key} <- {:api_key, get_api_key_header(conn)}, + {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)}, + {:already_verified, smart_contract} when is_nil(smart_contract) <- + {:already_verified, SmartContract.address_hash_to_smart_contract_without_twin(address_hash, @api_true)} do + creation_tx_input = contract_creation_input(address.hash) + + with {:ok, %{"sourceType" => type} = source} <- + %{} + |> prepare_bytecode_for_microservice(creation_tx_input, Data.to_string(address.contract_code)) + |> EthBytecodeDBInterface.search_contract_in_eth_bytecode_internal_db(), + {:ok, _} <- LookUpSmartContractSourcesOnDemand.process_contract_source(type, source, address.hash) do + conn + |> put_view(ApiView) + |> render(:message, %{message: "Success"}) + else + _ -> + conn + |> put_view(ApiView) + |> render(:message, %{message: "Contract was not imported"}) + end + end + end + defp valid_url?(url) when is_binary(url) do uri = URI.parse(url) uri.scheme != nil && uri.host =~ "." @@ -74,4 +119,14 @@ defmodule BlockScoutWeb.API.V2.ImportController do end defp put_token_string_field(changeset, _token_symbol, _field), do: changeset + + defp get_api_key_header(conn) do + case get_req_header(conn, "x-api-key") do + [api_key] -> + api_key + + _ -> + nil + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index ceec1582e098..e6fdbe0996cc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -2,10 +2,12 @@ defmodule BlockScoutWeb.API.V2.MainPageController do use Phoenix.Controller alias Explorer.{Chain, PagingOptions} - alias BlockScoutWeb.API.V2.{BlockView, TransactionView} + alias BlockScoutWeb.API.V2.{BlockView, OptimismView, TransactionView} alias Explorer.{Chain, Repo} + alias Explorer.Chain.Optimism.Deposit import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] @transactions_options [ necessity_by_association: %{ @@ -32,7 +34,20 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(BlockView) - |> render(:blocks, %{blocks: blocks}) + |> render(:blocks, %{blocks: blocks |> maybe_preload_ens()}) + end + + def optimism_deposits(conn, _params) do + recent_deposits = + Deposit.list( + paging_options: %PagingOptions{page_size: 6}, + api?: true + ) + + conn + |> put_status(200) + |> put_view(OptimismView) + |> render(:optimism_deposits, %{deposits: recent_deposits}) end def transactions(conn, _params) do @@ -41,7 +56,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions, %{transactions: recent_transactions}) + |> render(:transactions, %{transactions: recent_transactions |> maybe_preload_ens()}) end def watchlist_transactions(conn, _params) do @@ -51,7 +66,10 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions_watchlist, %{transactions: transactions, watchlist_names: watchlist_names}) + |> render(:transactions_watchlist, %{ + transactions: transactions |> maybe_preload_ens(), + watchlist_names: watchlist_names + }) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex new file mode 100644 index 000000000000..ef3bfe0d688e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -0,0 +1,111 @@ +defmodule BlockScoutWeb.API.V2.OptimismController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Chain, + only: [ + next_page_params: 3, + paging_options: 1, + split_list_by_page: 1 + ] + + alias Explorer.Chain + alias Explorer.Chain.Optimism.{Deposit, OutputRoot, TxnBatch, Withdrawal} + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + def txn_batches(conn, params) do + {batches, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> TxnBatch.list() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, batches, params) + + conn + |> put_status(200) + |> render(:optimism_txn_batches, %{ + batches: batches, + next_page_params: next_page_params + }) + end + + def txn_batches_count(conn, _params) do + items_count(conn, TxnBatch) + end + + def output_roots(conn, params) do + {roots, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> OutputRoot.list() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, roots, params) + + conn + |> put_status(200) + |> render(:optimism_output_roots, %{ + roots: roots, + next_page_params: next_page_params + }) + end + + def output_roots_count(conn, _params) do + items_count(conn, OutputRoot) + end + + def deposits(conn, params) do + {deposits, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> Deposit.list() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, deposits, params) + + conn + |> put_status(200) + |> render(:optimism_deposits, %{ + deposits: deposits, + next_page_params: next_page_params + }) + end + + def deposits_count(conn, _params) do + items_count(conn, Deposit) + end + + def withdrawals(conn, params) do + {withdrawals, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> Withdrawal.list() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, withdrawals, params) + + conn + |> put_status(200) + |> render(:optimism_withdrawals, %{ + withdrawals: withdrawals, + next_page_params: next_page_params + }) + end + + def withdrawals_count(conn, _params) do + items_count(conn, Withdrawal) + end + + defp items_count(conn, module) do + count = Chain.get_table_rows_total_count(module, api?: true) + + conn + |> put_status(200) + |> render(:optimism_items_count, %{count: count}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_zkevm_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_zkevm_controller.ex new file mode 100644 index 000000000000..e01b9a7caf9c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_zkevm_controller.ex @@ -0,0 +1,180 @@ +defmodule BlockScoutWeb.API.V2.PolygonZkevmController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Chain, + only: [ + next_page_params: 3, + paging_options: 1, + split_list_by_page: 1 + ] + + alias Explorer.Chain.PolygonZkevm.Reader + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @batch_necessity_by_association %{ + :sequence_transaction => :optional, + :verify_transaction => :optional, + :l2_transactions => :optional + } + + @batches_necessity_by_association %{ + :sequence_transaction => :optional, + :verify_transaction => :optional + } + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/batches/:batch_number` endpoint. + """ + @spec batch(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batch(conn, %{"batch_number" => batch_number} = _params) do + case Reader.batch( + batch_number, + necessity_by_association: @batch_necessity_by_association, + api?: true + ) do + {:ok, batch} -> + conn + |> put_status(200) + |> render(:zkevm_batch, %{batch: batch}) + + {:error, :not_found} = res -> + res + end + end + + @doc """ + Function to handle GET requests to `/api/v2/main-page/zkevm/batches/latest-number` endpoint. + """ + @spec batch_latest_number(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batch_latest_number(conn, _params) do + conn + |> put_status(200) + |> render(:zkevm_batch_latest_number, %{number: batch_latest_number()}) + end + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/batches` endpoint. + """ + @spec batches(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batches(conn, params) do + {batches, next_page} = + params + |> paging_options() + |> Keyword.put(:necessity_by_association, @batches_necessity_by_association) + |> Keyword.put(:api?, true) + |> Reader.batches() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, batches, params) + + conn + |> put_status(200) + |> render(:zkevm_batches, %{ + batches: batches, + next_page_params: next_page_params + }) + end + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/batches/count` endpoint. + """ + @spec batches_count(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batches_count(conn, _params) do + conn + |> put_status(200) + |> render(:zkevm_batches_count, %{count: batch_latest_number()}) + end + + @doc """ + Function to handle GET requests to `/api/v2/main-page/zkevm/batches/confirmed` endpoint. + """ + @spec batches_confirmed(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batches_confirmed(conn, _params) do + batches = + [] + |> Keyword.put(:necessity_by_association, @batches_necessity_by_association) + |> Keyword.put(:api?, true) + |> Keyword.put(:confirmed?, true) + |> Reader.batches() + + conn + |> put_status(200) + |> render(:zkevm_batches, %{batches: batches}) + end + + defp batch_latest_number do + case Reader.batch(:latest, api?: true) do + {:ok, batch} -> batch.number + {:error, :not_found} -> 0 + end + end + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/deposits` endpoint. + """ + @spec deposits(Plug.Conn.t(), map()) :: Plug.Conn.t() + def deposits(conn, params) do + {deposits, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> Reader.deposits() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, deposits, params) + + conn + |> put_status(200) + |> render(:polygon_zkevm_bridge_items, %{ + items: deposits, + next_page_params: next_page_params + }) + end + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/deposits/count` endpoint. + """ + @spec deposits_count(Plug.Conn.t(), map()) :: Plug.Conn.t() + def deposits_count(conn, _params) do + count = Reader.deposits_count(api?: true) + + conn + |> put_status(200) + |> render(:polygon_zkevm_bridge_items_count, %{count: count}) + end + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/withdrawals` endpoint. + """ + @spec withdrawals(Plug.Conn.t(), map()) :: Plug.Conn.t() + def withdrawals(conn, params) do + {withdrawals, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> Reader.withdrawals() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, withdrawals, params) + + conn + |> put_status(200) + |> render(:polygon_zkevm_bridge_items, %{ + items: withdrawals, + next_page_params: next_page_params + }) + end + + @doc """ + Function to handle GET requests to `/api/v2/zkevm/withdrawals/count` endpoint. + """ + @spec withdrawals_count(Plug.Conn.t(), map()) :: Plug.Conn.t() + def withdrawals_count(conn, _params) do + count = Reader.withdrawals_count(api?: true) + + conn + |> put_status(200) + |> render(:polygon_zkevm_bridge_items_count, %{count: count}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex new file mode 100644 index 000000000000..f3e996de3d20 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex @@ -0,0 +1,238 @@ +defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.API.V2.Helper + alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService + alias Explorer.Chain + alias Explorer.MicroserviceInterfaces.AccountAbstraction + + @address_fields ["bundler", "entry_point", "sender", "address", "factory", "paymaster"] + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations/:user_operation_hash_param` endpoint. + """ + @spec operation(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def operation(conn, %{"operation_hash_param" => operation_hash_string}) do + operation_hash_string + |> AccountAbstraction.get_user_ops_by_hash() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations/:user_operation_hash_param/summary` endpoint. + """ + @spec summary(Plug.Conn.t(), map()) :: + {:error | :format | :tx_interpreter_enabled | non_neg_integer(), any()} | Plug.Conn.t() + def summary(conn, %{"operation_hash_param" => operation_hash_string, "just_request_body" => "true"}) do + with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_transaction_hash(operation_hash_string)}, + {200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do + conn + |> json(TransactionInterpretationService.get_user_op_request_body(user_op)) + end + end + + def summary(conn, %{"operation_hash_param" => operation_hash_string}) do + with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_transaction_hash(operation_hash_string)}, + {:tx_interpreter_enabled, true} <- {:tx_interpreter_enabled, TransactionInterpretationService.enabled?()}, + {200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do + {response, code} = + case TransactionInterpretationService.interpret_user_operation(user_op) do + {:ok, response} -> {response, 200} + {:error, %Jason.DecodeError{}} -> {%{error: "Error while tx interpreter response decoding"}, 500} + {{:error, error}, code} -> {%{error: error}, code} + end + + conn + |> put_status(code) + |> json(response) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundlers/:address_hash_param` endpoint. + """ + @spec bundler(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def bundler(conn, %{"address_hash_param" => address_hash_string}) do + address_hash_string + |> AccountAbstraction.get_bundler_by_hash() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundlers` endpoint. + """ + @spec bundlers(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def bundlers(conn, query_string) do + query_string + |> AccountAbstraction.get_bundlers() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/factories/:address_hash_param` endpoint. + """ + @spec factory(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def factory(conn, %{"address_hash_param" => address_hash_string}) do + address_hash_string + |> AccountAbstraction.get_factory_by_hash() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/factories` endpoint. + """ + @spec factories(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def factories(conn, query_string) do + query_string + |> AccountAbstraction.get_factories() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/paymasters/:address_hash_param` endpoint. + """ + @spec paymaster(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def paymaster(conn, %{"address_hash_param" => address_hash_string}) do + address_hash_string + |> AccountAbstraction.get_paymaster_by_hash() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/paymasters` endpoint. + """ + @spec paymasters(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def paymasters(conn, query_string) do + query_string + |> AccountAbstraction.get_paymasters() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/accounts/:address_hash_param` endpoint. + """ + @spec account(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def account(conn, %{"address_hash_param" => address_hash_string}) do + address_hash_string + |> AccountAbstraction.get_account_by_hash() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/accounts` endpoint. + """ + @spec accounts(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def accounts(conn, query_string) do + query_string + |> AccountAbstraction.get_accounts() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundles` endpoint. + """ + @spec bundles(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def bundles(conn, query_string) do + query_string + |> AccountAbstraction.get_bundles() + |> process_response(conn) + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations` endpoint. + """ + @spec operations(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def operations(conn, query_string) do + query_string + |> AccountAbstraction.get_operations() + |> process_response(conn) + end + + defp extended_info(response) do + address_hashes = + response + |> collect_address_hashes() + |> Chain.hashes_to_addresses( + necessity_by_association: %{ + :names => :optional, + :smart_contract => :optional + }, + api?: true + ) + |> Enum.into(%{}, &{&1.hash, Helper.address_with_info(&1, nil)}) + + response |> replace_address_hashes(address_hashes) + end + + defp collect_address_hashes(response) do + address_hash_strings = + case response do + %{"items" => items} -> + @address_fields |> Enum.flat_map(fn field -> Enum.map(items, & &1[field]) end) + + item -> + @address_fields |> Enum.map(&item[&1]) + end + + address_hash_strings + |> Enum.filter(&(!is_nil(&1))) + |> Enum.uniq() + |> Enum.map(fn hash_string -> + case Chain.string_to_address_hash(hash_string) do + {:ok, hash} -> hash + _ -> nil + end + end) + |> Enum.filter(&(!is_nil(&1))) + end + + defp replace_address_hashes(response, addresses) do + case response do + %{"items" => items} -> + extended_items = items |> Enum.map(&add_address_extended_info(&1, addresses)) + + response |> Map.put("items", extended_items) + + item -> + add_address_extended_info(item, addresses) + end + end + + defp add_address_extended_info(response, addresses) do + @address_fields + |> Enum.reduce(response, fn address_output_field, output_response -> + with true <- Map.has_key?(output_response, address_output_field), + {:ok, address_hash} <- output_response |> Map.get(address_output_field) |> Chain.string_to_address_hash(), + true <- Map.has_key?(addresses, address_hash) do + output_response |> Map.replace(address_output_field, Map.get(addresses, address_hash)) + else + _ -> output_response + end + end) + end + + defp process_response(response, conn) do + case response do + {:error, :disabled} -> + conn + |> put_status(501) + |> json(%{message: "Service is disabled"}) + + {status_code, response} -> + final_json = response |> extended_info() |> try_to_decode_call_data() + + conn + |> put_status(status_code) + |> json(final_json) + end + end + + defp try_to_decode_call_data(%{"call_data" => _call_data} = user_op) do + {_mock_tx, _decoded_input, decoded_input_json} = TransactionInterpretationService.decode_user_op_calldata(user_op) + Map.put(user_op, "decoded_call_data", decoded_input_json) + end + + defp try_to_decode_call_data(response), do: response +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex new file mode 100644 index 000000000000..69463dae21f8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex @@ -0,0 +1,57 @@ +defmodule BlockScoutWeb.API.V2.Proxy.NovesFiController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.API.V2.{AddressController, TransactionController} + alias Explorer.ThirdPartyIntegrations.NovesFi + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @doc """ + Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions/:transaction_hash_param` endpoint. + """ + @spec transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:ok, _transaction, _transaction_hash} <- + TransactionController.validate_transaction(transaction_hash_string, params, + necessity_by_association: %{}, + api?: true + ), + url = NovesFi.tx_url(transaction_hash_string), + {response, status} <- NovesFi.noves_fi_api_request(url, conn), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(status) + |> json(response) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions/:transaction_hash_param/transaction` endpoint. + """ + @spec address_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def address_transactions(conn, %{"address_hash_param" => address_hash_string} = params) do + with {:ok, _address_hash, _address} <- AddressController.validate_address(address_hash_string, params), + url = NovesFi.address_txs_url(address_hash_string), + {response, status} <- NovesFi.noves_fi_api_request(url, conn), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(status) + |> json(response) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions` endpoint. + """ + @spec describe_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def describe_transactions(conn, _) do + url = NovesFi.describe_txs_url() + + with {response, status} <- NovesFi.noves_fi_api_request(url, conn, :post_transactions), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(status) + |> json(response) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex index abe0aca31486..0a3c0f17aed4 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.SearchController do use Phoenix.Controller import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1, from_param: 1] + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens_info_to_search_results: 1] alias Explorer.Chain.Search alias Explorer.PagingOptions @@ -22,7 +23,10 @@ defmodule BlockScoutWeb.API.V2.SearchController do conn |> put_status(200) - |> render(:search_results, %{search_results: search_results, next_page_params: next_page_params}) + |> render(:search_results, %{ + search_results: search_results |> maybe_preload_ens_info_to_search_results(), + next_page_params: next_page_params + }) end def check_redirect(conn, %{"q" => query}) do @@ -41,6 +45,6 @@ defmodule BlockScoutWeb.API.V2.SearchController do conn |> put_status(200) - |> render(:search_results, %{search_results: search_results}) + |> render(:search_results, %{search_results: search_results |> maybe_preload_ens_info_to_search_results()}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/shibarium_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/shibarium_controller.ex new file mode 100644 index 000000000000..9a57424fece2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/shibarium_controller.ex @@ -0,0 +1,79 @@ +defmodule BlockScoutWeb.API.V2.ShibariumController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Chain, + only: [ + next_page_params: 3, + paging_options: 1, + split_list_by_page: 1 + ] + + alias Explorer.Chain.Cache.ShibariumCounter + alias Explorer.Chain.Shibarium.Reader + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @spec deposits(Plug.Conn.t(), map()) :: Plug.Conn.t() + def deposits(conn, params) do + {deposits, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> Reader.deposits() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, deposits, params) + + conn + |> put_status(200) + |> render(:shibarium_deposits, %{ + deposits: deposits, + next_page_params: next_page_params + }) + end + + @spec deposits_count(Plug.Conn.t(), map()) :: Plug.Conn.t() + def deposits_count(conn, _params) do + count = + case ShibariumCounter.deposits_count(api?: true) do + 0 -> Reader.deposits_count(api?: true) + value -> value + end + + conn + |> put_status(200) + |> render(:shibarium_items_count, %{count: count}) + end + + @spec withdrawals(Plug.Conn.t(), map()) :: Plug.Conn.t() + def withdrawals(conn, params) do + {withdrawals, next_page} = + params + |> paging_options() + |> Keyword.put(:api?, true) + |> Reader.withdrawals() + |> split_list_by_page() + + next_page_params = next_page_params(next_page, withdrawals, params) + + conn + |> put_status(200) + |> render(:shibarium_withdrawals, %{ + withdrawals: withdrawals, + next_page_params: next_page_params + }) + end + + @spec withdrawals_count(Plug.Conn.t(), map()) :: Plug.Conn.t() + def withdrawals_count(conn, _params) do + count = + case ShibariumCounter.withdrawals_count(api?: true) do + 0 -> Reader.withdrawals_count(api?: true) + value -> value + end + + conn + |> put_status(200) + |> render(:shibarium_items_count, %{count: count}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index d9c60969a48c..9dbc80236b08 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] import BlockScoutWeb.PagingHelper, - only: [current_filter: 1, delete_parameters_from_next_page_params: 1, search_query: 1] + only: [current_filter: 1, delete_parameters_from_next_page_params: 1, search_query: 1, smart_contracts_sorting: 1] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] import Explorer.SmartContract.Solidity.Verifier, only: [parse_boolean: 1] @@ -12,14 +12,16 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do alias BlockScoutWeb.{AccessHelper, AddressView} alias Ecto.Association.NotLoaded alias Explorer.Chain - alias Explorer.Chain.SmartContract + alias Explorer.Chain.{Address, SmartContract} + alias Explorer.Chain.SmartContract.AuditReport alias Explorer.SmartContract.{Reader, Writer} alias Explorer.SmartContract.Solidity.PublishHelper + alias Explorer.ThirdPartyIntegrations.SolidityScan @smart_contract_address_options [ necessity_by_association: %{ :contracts_creation_internal_transaction => :optional, - :smart_contract => :optional, + [smart_contract: :smart_contract_additional_sources] => :optional, :contracts_creation_transaction => :optional }, api?: true @@ -47,7 +49,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string), {:not_found, true} <- {:not_found, AddressView.check_custom_abi_for_having_read_functions(custom_abi)} do read_only_functions_from_abi = - Reader.read_only_functions_from_abi_with_sender(custom_abi.abi, address_hash, params["from"]) + Reader.read_only_functions_from_abi_with_sender(custom_abi.abi, address_hash, params["from"], @api_true) read_functions_required_wallet_from_abi = Reader.read_functions_required_wallet_from_abi(custom_abi.abi) @@ -58,11 +60,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end def methods_read(conn, %{"address_hash" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - smart_contract <- Chain.address_hash_to_smart_contract(address_hash, @api_true), - {:not_found, false} <- {:not_found, is_nil(smart_contract)} do - read_only_functions_from_abi = Reader.read_only_functions(smart_contract, address_hash, params["from"]) + with {:ok, address_hash, smart_contract} <- validate_smart_contract(params, address_hash_string) do + read_only_functions_from_abi = Reader.read_only_functions(smart_contract, address_hash, params["from"], @api_true) read_functions_required_wallet_from_abi = Reader.read_functions_required_wallet(smart_contract) @@ -88,10 +87,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do def methods_write(conn, %{"address_hash" => address_hash_string} = params) do with {:contract_interaction_disabled, false} <- {:contract_interaction_disabled, AddressView.contract_interaction_disabled?()}, - {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - smart_contract <- Chain.address_hash_to_smart_contract(address_hash, @api_true), - {:not_found, false} <- {:not_found, is_nil(smart_contract)} do + {:ok, _address_hash, smart_contract} <- validate_smart_contract(params, address_hash_string) do conn |> put_status(200) |> json(smart_contract |> Writer.write_functions() |> Reader.get_abi_with_method_id()) @@ -170,7 +166,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do address_hash, %{method_id: params["method_id"], args: prepare_args(args)}, params["from"], - custom_abi.abi + custom_abi.abi, + @api_true ) else Reader.query_function_with_names( @@ -190,15 +187,44 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end end + @doc """ + /api/v2/smart-contracts/${address_hash_string}/solidityscan-report logic + """ + @spec solidityscan_report(Plug.Conn.t(), map()) :: + {:address, {:error, :not_found}} + | {:format_address, :error} + | {:is_empty_response, true} + | {:is_smart_contract, false | nil} + | {:restricted_access, true} + | {:is_verified_smart_contract, false} + | {:is_vyper_contract, true} + | Plug.Conn.t() + def solidityscan_report(conn, %{"address_hash" => address_hash_string} = params) do + with {:format_address, {:ok, address_hash}} <- {:format_address, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:address, {:ok, address}} <- {:address, Chain.hash_to_address(address_hash)}, + {:is_smart_contract, true} <- {:is_smart_contract, Address.smart_contract?(address)}, + smart_contract = SmartContract.address_hash_to_smart_contract_without_twin(address_hash, @api_true), + {:is_verified_smart_contract, true} <- {:is_verified_smart_contract, !is_nil(smart_contract)}, + {:is_vyper_contract, false} <- {:is_vyper_contract, smart_contract.is_vyper_contract}, + response = SolidityScan.solidityscan_request(address_hash_string), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(200) + |> json(response) + end + end + def smart_contracts_list(conn, params) do full_options = - [necessity_by_association: %{[address: :token] => :optional, [address: :names] => :optional}] + [necessity_by_association: %{[address: :token] => :optional, [address: :names] => :optional, address: :required}] |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) |> Keyword.merge(search_query(params)) + |> Keyword.merge(smart_contracts_sorting(params)) |> Keyword.merge(@api_true) - smart_contracts_plus_one = Chain.verified_contracts(full_options) + smart_contracts_plus_one = SmartContract.verified_contracts(full_options) {smart_contracts, next_page} = split_list_by_page(smart_contracts_plus_one) next_page_params = @@ -210,6 +236,58 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do |> render(:smart_contracts, %{smart_contracts: smart_contracts, next_page_params: next_page_params}) end + @doc """ + POST /api/v2/smart-contracts/{address_hash}/audit-reports + """ + @spec audit_report_submission(Plug.Conn.t(), map()) :: + {:error, Ecto.Changeset.t()} + | {:format, :error} + | {:not_found, nil | Explorer.Chain.SmartContract.t()} + | {:recaptcha, any()} + | {:restricted_access, true} + | Plug.Conn.t() + def audit_report_submission(conn, %{"address_hash" => address_hash_string} = params) do + captcha_helper = Application.get_env(:block_scout_web, :captcha_helper) + + with {:disabled, true} <- {:disabled, Application.get_env(:explorer, :air_table_audit_reports)[:enabled]}, + {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string), + {:recaptcha, _} <- {:recaptcha, captcha_helper.recaptcha_passed?(params["recaptcha_response"])}, + audit_report_params <- %{ + address_hash: address_hash, + submitter_name: params["submitter_name"], + submitter_email: params["submitter_email"], + is_project_owner: params["is_project_owner"], + project_name: params["project_name"], + project_url: params["project_url"], + audit_company_name: params["audit_company_name"], + audit_report_url: params["audit_report_url"], + audit_publish_date: params["audit_publish_date"], + comment: params["comment"] + }, + {:ok, _} <- AuditReport.create(audit_report_params) do + conn + |> put_status(200) + |> json(%{message: "OK"}) + end + end + + @doc """ + GET /api/v2/smart-contracts/{address_hash}/audit-reports + """ + @spec audit_reports_list(Plug.Conn.t(), map()) :: + {:format, :error} + | {:not_found, nil | Explorer.Chain.SmartContract.t()} + | {:restricted_access, true} + | Plug.Conn.t() + def audit_reports_list(conn, %{"address_hash" => address_hash_string} = params) do + with {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string) do + reports = AuditReport.get_audit_reports_by_smart_contract_address_hash(address_hash, @api_true) + + conn + |> render(:audit_reports, %{reports: reports}) + end + end + def smart_contracts_counters(conn, _params) do conn |> json(%{ @@ -222,4 +300,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do def prepare_args(list) when is_list(list), do: list def prepare_args(other), do: [other] + + defp validate_smart_contract(params, address_hash_string) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:not_found, smart_contract} when not is_nil(smart_contract) <- + {:not_found, SmartContract.address_hash_to_smart_contract(address_hash, @api_true)} do + {:ok, address_hash, smart_contract} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 825d9f1b2575..8cf9adbe5d8d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -3,15 +3,15 @@ defmodule BlockScoutWeb.API.V2.StatsController do alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.Chain.MarketHistoryChartController - alias EthereumJSONRPC.Variant alias Explorer.{Chain, Market} alias Explorer.Chain.Address.Counters alias Explorer.Chain.Cache.Block, as: BlockCache - alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage, RootstockLockedBTC} + alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage} alias Explorer.Chain.Cache.Transaction, as: TransactionCache alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Counters.AverageBlockTime + alias Plug.Conn alias Timex.Duration @api_true [api?: true] @@ -26,7 +26,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do :standard end - exchange_rate_from_db = Market.get_native_coin_exchange_rate_from_db() + exchange_rate = Market.get_coin_exchange_rate() transaction_stats = Helper.get_transaction_stats() @@ -39,6 +39,21 @@ defmodule BlockScoutWeb.API.V2.StatsController do nil end + coin_price_change = + case Market.fetch_recent_history() do + [today, yesterday | _] -> + today.closing_price && yesterday.closing_price && + today.closing_price + |> Decimal.div(yesterday.closing_price) + |> Decimal.sub(1) + |> Decimal.mult(100) + |> Decimal.to_float() + |> Float.ceil(2) + + _ -> + nil + end + gas_price = Application.get_env(:block_scout_web, :gas_price) json( @@ -48,17 +63,22 @@ defmodule BlockScoutWeb.API.V2.StatsController do "total_addresses" => @api_true |> Counters.address_estimated_count() |> to_string(), "total_transactions" => TransactionCache.estimated_count() |> to_string(), "average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(), - "coin_price" => exchange_rate_from_db.usd_value, + "coin_image" => exchange_rate.image_url, + "coin_price" => exchange_rate.usd_value, + "coin_price_change_percentage" => coin_price_change, "total_gas_used" => GasUsage.total() |> to_string(), "transactions_today" => Enum.at(transaction_stats, 0).number_of_transactions |> to_string(), "gas_used_today" => Enum.at(transaction_stats, 0).gas_used, "gas_prices" => gas_prices, + "gas_prices_update_in" => GasPriceOracle.update_in(), + "gas_price_updated_at" => GasPriceOracle.get_updated_at(), "static_gas_price" => gas_price, - "market_cap" => Helper.market_cap(market_cap_type, exchange_rate_from_db), - "tvl" => exchange_rate_from_db.tvl_usd, + "market_cap" => Helper.market_cap(market_cap_type, exchange_rate), + "tvl" => exchange_rate.tvl_usd, "network_utilization_percentage" => network_utilization_percentage() } - |> add_rootstock_locked_btc() + |> add_chain_type_fields() + |> backward_compatibility(conn) ) end @@ -127,12 +147,53 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end - defp add_rootstock_locked_btc(stats) do - with "rsk" <- Variant.get(), - rootstock_locked_btc when not is_nil(rootstock_locked_btc) <- RootstockLockedBTC.get_locked_value() do - stats |> Map.put("rootstock_locked_btc", rootstock_locked_btc) - else - _ -> stats + def secondary_coin_market_chart(conn, _params) do + recent_market_history = Market.fetch_recent_history(true) + + chart_data = + recent_market_history + |> Enum.map(fn day -> Map.take(day, [:closing_price, :date]) end) + + json(conn, %{ + chart_data: chart_data + }) + end + + defp backward_compatibility(response, conn) do + case Conn.get_req_header(conn, "updated-gas-oracle") do + ["true"] -> + response + + _ -> + response + |> Map.update("gas_prices", nil, fn + gas_prices -> + %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]} + end) end end + + case Application.compile_env(:explorer, :chain_type) do + "rsk" -> + defp add_chain_type_fields(response) do + alias Explorer.Chain.Cache.RootstockLockedBTC + + case RootstockLockedBTC.get_locked_value() do + rootstock_locked_btc when not is_nil(rootstock_locked_btc) -> + response |> Map.put("rootstock_locked_btc", rootstock_locked_btc) + + _ -> + response + end + end + + "optimism" -> + defp add_chain_type_fields(response) do + import Explorer.Counters.LastOutputRootSizeCounter, only: [fetch: 1] + response |> Map.put("last_output_root_size", fetch(@api_true)) + end + + _ -> + defp add_chain_type_fields(response), do: response + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 1ec2a32a3e0b..0be915d1e4ac 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -2,8 +2,9 @@ defmodule BlockScoutWeb.API.V2.TokenController do use BlockScoutWeb, :controller alias BlockScoutWeb.AccessHelper - alias BlockScoutWeb.API.V2.TransactionView - alias Explorer.Chain + alias BlockScoutWeb.API.V2.{AddressView, TransactionView} + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} alias Indexer.Fetcher.TokenTotalSupplyOnDemand import BlockScoutWeb.Chain, @@ -17,7 +18,14 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] import BlockScoutWeb.PagingHelper, - only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1, tokens_sorting: 1] + only: [ + chain_ids_filter_options: 1, + delete_parameters_from_next_page_params: 1, + token_transfers_types_options: 1, + tokens_sorting: 1 + ] + + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] action_fallback(BlockScoutWeb.API.V2.FallbackController) @@ -29,6 +37,27 @@ defmodule BlockScoutWeb.API.V2.TokenController do {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do TokenTotalSupplyOnDemand.trigger_fetch(address_hash) + conn + |> token_response(token, address_hash) + end + end + + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + defp token_response(conn, token, address_hash) do + if token.bridged do + bridged_token = Repo.get_by(BridgedToken, home_token_contract_address_hash: address_hash) + + conn + |> put_status(200) + |> render(:bridged_token, %{token: {token, bridged_token}}) + else + conn + |> put_status(200) + |> render(:token, %{token: token}) + end + end + else + defp token_response(conn, token, _address_hash) do conn |> put_status(200) |> render(:token, %{token: token}) @@ -66,7 +95,10 @@ defmodule BlockScoutWeb.API.V2.TokenController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) + |> render(:token_transfers, %{ + token_transfers: token_transfers |> maybe_preload_ens(), + next_page_params: next_page_params + }) end end @@ -83,7 +115,48 @@ defmodule BlockScoutWeb.API.V2.TokenController do conn |> put_status(200) - |> render(:token_balances, %{token_balances: token_balances, next_page_params: next_page_params, token: token}) + |> render(:token_balances, %{ + token_balances: token_balances |> maybe_preload_ens(), + next_page_params: next_page_params, + token: token + }) + end + end + + def instances( + conn, + %{"address_hash_param" => address_hash_string, "holder_address_hash" => holder_address_hash_string} = params + ) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:format, {:ok, holder_address_hash}} <- {:format, Chain.string_to_address_hash(holder_address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(holder_address_hash_string, params) do + holder_address = Repo.get_by(Address, hash: holder_address_hash) + + results_plus_one = + Instance.token_instances_by_holder_address_hash( + token, + holder_address_hash, + params + |> unique_tokens_paging_options() + |> Keyword.merge(@api_true) + ) + + {token_instances, next_page} = split_list_by_page(results_plus_one) + + next_page_params = + next_page |> unique_tokens_next_page(token_instances, delete_parameters_from_next_page_params(params)) + + conn + |> put_status(200) + |> put_view(AddressView) + |> render(:nft_list, %{ + token_instances: token_instances |> put_owner(holder_address), + next_page_params: next_page_params, + token: token + }) end end @@ -94,6 +167,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do results_plus_one = Chain.address_to_unique_tokens( token.contract_address_hash, + token, Keyword.merge(unique_tokens_paging_options(params), @api_true) ) @@ -112,12 +186,24 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.is_erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do token_instance = - case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do - {:ok, token_instance} -> token_instance |> Chain.put_owner_to_token_instance(@api_true) - {:error, :not_found} -> %{token_id: token_id, metadata: nil, owner: nil} + case Chain.nft_instance_from_token_id_and_token_address(token_id, address_hash, @api_true) do + {:ok, token_instance} -> + token_instance + |> Chain.select_repo(@api_true).preload(:owner) + |> Chain.put_owner_to_token_instance(token, @api_true) + + {:error, :not_found} -> + %Instance{ + token_id: Decimal.new(token_id), + metadata: nil, + owner: nil, + token_contract_address_hash: address_hash + } + |> Instance.put_is_unique(token, @api_true) + |> Chain.put_owner_to_token_instance(token, @api_true) end conn @@ -133,7 +219,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.is_erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do paging_options = paging_options(params) @@ -152,7 +238,10 @@ defmodule BlockScoutWeb.API.V2.TokenController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) + |> render(:token_transfers, %{ + token_transfers: token_transfers |> maybe_preload_ens(), + next_page_params: next_page_params + }) end end @@ -160,7 +249,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.is_erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do paging_options = paging_options(params) @@ -179,7 +268,11 @@ defmodule BlockScoutWeb.API.V2.TokenController do conn |> put_status(200) - |> render(:token_balances, %{token_balances: token_holders, next_page_params: next_page_params, token: token}) + |> render(:token_balances, %{ + token_balances: token_holders |> maybe_preload_ens(), + next_page_params: next_page_params, + token: token + }) end end @@ -190,7 +283,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.is_erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_str)} do conn |> put_status(200) @@ -210,7 +303,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> Keyword.merge(tokens_sorting(params)) |> Keyword.merge(@api_true) - {tokens, next_page} = filter |> Chain.list_top_tokens(options) |> split_list_by_page() + {tokens, next_page} = filter |> Token.list_top(options) |> split_list_by_page() next_page_params = next_page |> next_page_params(tokens, delete_parameters_from_next_page_params(params)) @@ -218,4 +311,26 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> put_status(200) |> render(:tokens, %{tokens: tokens, next_page_params: next_page_params}) end + + def bridged_tokens_list(conn, params) do + filter = params["q"] + + options = + params + |> paging_options() + |> Keyword.merge(chain_ids_filter_options(params)) + |> Keyword.merge(tokens_sorting(params)) + |> Keyword.merge(@api_true) + + {tokens, next_page} = filter |> BridgedToken.list_top_bridged_tokens(options) |> split_list_by_page() + + next_page_params = next_page |> next_page_params(tokens, delete_parameters_from_next_page_params(params)) + + conn + |> put_status(200) + |> render(:bridged_tokens, %{tokens: tokens, next_page_params: next_page_params}) + end + + defp put_owner(token_instances, holder_address), + do: Enum.map(token_instances, fn token_instance -> %Instance{token_instance | owner: holder_address} end) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 43b63b6f693d..05039db280e6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do use BlockScoutWeb, :controller import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + alias BlockScoutWeb.API.V2.BlobView import BlockScoutWeb.Chain, only: [ @@ -22,23 +23,41 @@ defmodule BlockScoutWeb.API.V2.TransactionController do type_filter_options: 1 ] + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1, maybe_preload_ens_to_transaction: 1] + alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService alias BlockScoutWeb.Models.TransactionStateHelper alias Explorer.Chain - alias Explorer.Chain.Zkevm.Reader + alias Explorer.Chain.Beacon.Reader, as: BeaconReader + alias Explorer.Chain.{Hash, Transaction} + alias Explorer.Chain.PolygonZkevm.Reader + alias Explorer.Chain.ZkSync.Reader + alias Explorer.Counters.{FreshPendingTransactionsCounter, Transactions24hStats} alias Indexer.Fetcher.FirstTraceOnDemand action_fallback(BlockScoutWeb.API.V2.FallbackController) + case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> + @chain_type_transaction_necessity_by_association %{ + :beacon_blob_transaction => :optional + } + + _ -> + @chain_type_transaction_necessity_by_association %{} + end + + # TODO might be redundant to preload blob fields in some of the endpoints @transaction_necessity_by_association %{ - :block => :optional, - [created_contract_address: :names] => :optional, - [created_contract_address: :token] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - # as far as I remember this needed for substituting implementation name in `to` address instead of is's real name (in transactions) - [to_address: :smart_contract] => :optional - } + :block => :optional, + [created_contract_address: :names] => :optional, + [created_contract_address: :token] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + [to_address: :smart_contract] => :optional + } + |> Map.merge(@chain_type_transaction_necessity_by_association) @token_transfers_necessity_by_association %{ [from_address: :smart_contract] => :optional, @@ -60,7 +79,6 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [created_contract_address: :names] => :optional, [from_address: :names] => :optional, [to_address: :names] => :optional, - [transaction: :block] => :optional, [created_contract_address: :smart_contract] => :optional, [from_address: :smart_contract] => :optional, [to_address: :smart_contract] => :optional @@ -85,6 +103,13 @@ defmodule BlockScoutWeb.API.V2.TransactionController do |> Map.put(:zkevm_sequence_transaction, :optional) |> Map.put(:zkevm_verify_transaction, :optional) + "zksync" -> + necessity_by_association_with_actions + |> Map.put(:zksync_batch, :optional) + |> Map.put(:zksync_commit_transaction, :optional) + |> Map.put(:zksync_prove_transaction, :optional) + |> Map.put(:zksync_execute_transaction, :optional) + "suave" -> necessity_by_association_with_actions |> Map.put(:logs, :optional) @@ -95,21 +120,16 @@ defmodule BlockScoutWeb.API.V2.TransactionController do necessity_by_association_with_actions end - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, - Chain.hash_to_transaction( - transaction_hash, - necessity_by_association: necessity_by_association, - api?: true - )}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params), + with {:ok, transaction, _transaction_hash} <- + validate_transaction(transaction_hash_string, params, + necessity_by_association: necessity_by_association, + api?: true + ), preloaded <- Chain.preload_token_transfers(transaction, @token_transfers_in_tx_necessity_by_association, @api_true, false) do conn |> put_status(200) - |> render(:transaction, %{transaction: preloaded}) + |> render(:transaction, %{transaction: preloaded |> maybe_preload_ens_to_transaction()}) end end @@ -137,15 +157,32 @@ defmodule BlockScoutWeb.API.V2.TransactionController do conn |> put_status(200) - |> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) + |> render(:transactions, %{transactions: transactions |> maybe_preload_ens(), next_page_params: next_page_params}) end @doc """ Function to handle GET requests to `/api/v2/transactions/zkevm-batch/:batch_number` endpoint. It renders the list of L2 transactions bound to the specified batch. """ - @spec zkevm_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() - def zkevm_batch(conn, %{"batch_number" => batch_number} = _params) do + @spec polygon_zkevm_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() + def polygon_zkevm_batch(conn, %{"batch_number" => batch_number} = _params) do + transactions = + batch_number + |> Reader.batch_transactions(api?: true) + |> Enum.map(fn tx -> tx.hash end) + |> Chain.hashes_to_transactions(api?: true, necessity_by_association: @transaction_necessity_by_association) + + conn + |> put_status(200) + |> render(:transactions, %{transactions: transactions |> maybe_preload_ens(), items: true}) + end + + @doc """ + Function to handle GET requests to `/api/v2/transactions/zksync-batch/:batch_number` endpoint. + It renders the list of L2 transactions bound to the specified batch. + """ + @spec zksync_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() + def zksync_batch(conn, %{"batch_number" => batch_number} = _params) do transactions = batch_number |> Reader.batch_transactions(api?: true) @@ -174,7 +211,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do conn |> put_status(200) - |> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) + |> render(:transactions, %{transactions: transactions |> maybe_preload_ens(), next_page_params: next_page_params}) end end @@ -183,11 +220,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec raw_trace(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def raw_trace(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do if is_nil(transaction.block_number) do conn |> put_status(200) @@ -216,11 +249,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec token_transfers(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def token_transfers(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do paging_options = paging_options(params) full_options = @@ -243,7 +272,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do conn |> put_status(200) - |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) + |> render(:token_transfers, %{ + token_transfers: token_transfers |> maybe_preload_ens(), + next_page_params: next_page_params + }) end end @@ -252,11 +284,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec internal_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def internal_transactions(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = @internal_transaction_necessity_by_association |> Keyword.merge(paging_options(params)) @@ -273,7 +301,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do conn |> put_status(200) |> render(:internal_transactions, %{ - internal_transactions: internal_transactions, + internal_transactions: internal_transactions |> maybe_preload_ens(), next_page_params: next_page_params }) end @@ -284,11 +312,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec logs(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def logs(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = [ necessity_by_association: %{ @@ -312,7 +336,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do |> put_status(200) |> render(:logs, %{ tx_hash: transaction_hash, - logs: logs, + logs: logs |> maybe_preload_ens(), next_page_params: next_page_params }) end @@ -323,16 +347,12 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec state_changes(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def state_changes(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, - Chain.hash_to_transaction(transaction_hash, - necessity_by_association: - Map.merge(@transaction_necessity_by_association, %{[block: [miner: :names]] => :optional}), - api?: true - )}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, transaction, _transaction_hash} <- + validate_transaction(transaction_hash_string, params, + necessity_by_association: + Map.merge(@transaction_necessity_by_association, %{[block: [miner: :names]] => :optional}), + api?: true + ) do state_changes_plus_next_page = transaction |> TransactionStateHelper.state_changes(params |> paging_options() |> Keyword.merge(api?: true)) @@ -370,10 +390,97 @@ defmodule BlockScoutWeb.API.V2.TransactionController do conn |> put_status(200) |> render(:transactions_watchlist, %{ - transactions: transactions, + transactions: transactions |> maybe_preload_ens(), next_page_params: next_page_params, watchlist_names: watchlist_names }) end end + + def summary(conn, %{"transaction_hash_param" => transaction_hash_string, "just_request_body" => "true"} = params) do + with {:tx_interpreter_enabled, true} <- {:tx_interpreter_enabled, TransactionInterpretationService.enabled?()}, + {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params) do + conn + |> json(TransactionInterpretationService.get_request_body(transaction)) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/summary` endpoint. + """ + @spec summary(Plug.Conn.t(), map()) :: + {:format, :error} + | {:not_found, {:error, :not_found}} + | {:restricted_access, true} + | {:tx_interpreter_enabled, boolean} + | Plug.Conn.t() + def summary(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:tx_interpreter_enabled, true} <- {:tx_interpreter_enabled, TransactionInterpretationService.enabled?()}, + {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params) do + {response, code} = + case TransactionInterpretationService.interpret(transaction) do + {:ok, response} -> {response, 200} + {:error, %Jason.DecodeError{}} -> {%{error: "Error while tx interpreter response decoding"}, 500} + {{:error, error}, code} -> {%{error: error}, code} + end + + conn + |> put_status(code) + |> json(response) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/blobs` endpoint. + """ + @spec blobs(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def blobs(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do + full_options = @api_true + + blobs = BeaconReader.transaction_to_blobs(transaction_hash, full_options) + + conn + |> put_status(200) + |> put_view(BlobView) + |> render(:blobs, %{blobs: blobs}) + end + end + + def stats(conn, _params) do + transactions_count = Transactions24hStats.fetch_count(@api_true) + pending_transactions_count = FreshPendingTransactionsCounter.fetch(@api_true) + transaction_fees_sum = Transactions24hStats.fetch_fee_sum(@api_true) + transaction_fees_avg = Transactions24hStats.fetch_fee_average(@api_true) + + conn + |> put_status(200) + |> render( + :stats, + %{ + transactions_count_24h: transactions_count, + pending_transactions_count: pending_transactions_count, + transaction_fees_sum_24h: transaction_fees_sum, + transaction_fees_avg_24h: transaction_fees_avg + } + ) + end + + @doc """ + Checks if this valid transaction hash string, and this transaction doesn't belong to prohibited address + """ + @spec validate_transaction(String.t(), any(), Keyword.t()) :: + {:format, :error} + | {:not_found, {:error, :not_found}} + | {:restricted_access, true} + | {:ok, Transaction.t(), Hash.t()} + def validate_transaction(transaction_hash_string, params, options \\ @api_true) do + with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, + {:not_found, {:ok, transaction}} <- + {:not_found, Chain.hash_to_transaction(transaction_hash, options)}, + {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), + {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + {:ok, transaction, transaction_hash} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex new file mode 100644 index 000000000000..90a10cff5900 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex @@ -0,0 +1,38 @@ +defmodule BlockScoutWeb.API.V2.UtilsController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.API.V2.TransactionView + alias Explorer.Chain + alias Explorer.Chain.{Address, Data, SmartContract, Transaction} + + @api_true [api?: true] + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @doc """ + Function to handle GET and POST requests to `/api/v2/utils/decode-calldata` + """ + @spec decode_calldata(Plug.Conn.t(), map()) :: {:format, :error} | Plug.Conn.t() + def decode_calldata(conn, params) do + with {:format, {:ok, data}} <- {:format, Data.cast(params["calldata"])}, + address_hash <- params["address_hash"] && Chain.string_to_address_hash(params["address_hash"]), + {:format, true} <- {:format, match?({:ok, _hash}, address_hash) || is_nil(address_hash)} do + smart_contract = + if address_hash, do: SmartContract.address_hash_to_smart_contract(elem(address_hash, 1), @api_true) + + {decoded_input, _abi_acc, _methods_acc} = + Transaction.decoded_input_data( + %Transaction{ + input: data, + to_address: %Address{contract_code: %Data{bytes: ""}, smart_contract: smart_contract} + }, + @api_true + ) + + decoded_input_data = decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input() + + conn + |> json(%{result: decoded_input_data}) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex new file mode 100644 index 000000000000..4c7883502814 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex @@ -0,0 +1,83 @@ +defmodule BlockScoutWeb.API.V2.ValidatorController do + use BlockScoutWeb, :controller + + alias Explorer.Chain.Cache.StabilityValidatorsCounters + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + + import BlockScoutWeb.PagingHelper, + only: [ + delete_parameters_from_next_page_params: 1, + stability_validators_state_options: 1, + validators_stability_sorting: 1 + ] + + import BlockScoutWeb.Chain, + only: [ + split_list_by_page: 1, + paging_options: 1, + next_page_params: 4 + ] + + @api_true api?: true + + @doc """ + Function to handle GET requests to `/api/v2/validators/stability` endpoint. + """ + @spec stability_validators_list(Plug.Conn.t(), map()) :: Plug.Conn.t() + def stability_validators_list(conn, params) do + options = + [ + necessity_by_association: %{ + :address => :optional + } + ] + |> Keyword.merge(@api_true) + |> Keyword.merge(paging_options(params)) + |> Keyword.merge(validators_stability_sorting(params)) + |> Keyword.merge(stability_validators_state_options(params)) + + {validators, next_page} = options |> ValidatorStability.get_paginated_validators() |> split_list_by_page() + + next_page_params = + next_page + |> next_page_params( + validators, + delete_parameters_from_next_page_params(params), + &ValidatorStability.next_page_params/1 + ) + + conn + |> render(:stability_validators, %{validators: validators, next_page_params: next_page_params}) + end + + @doc """ + Function to handle GET requests to `/api/v2/validators/stability/counters` endpoint. + """ + @spec stability_validators_counters(Plug.Conn.t(), map()) :: Plug.Conn.t() + def stability_validators_counters(conn, _params) do + %{ + validators_counter: validators_counter, + new_validators_counter: new_validators_counter, + active_validators_counter: active_validators_counter + } = StabilityValidatorsCounters.get_counters(@api_true) + + conn + |> json(%{ + validators_counter: validators_counter, + new_validators_counter_24h: new_validators_counter, + active_validators_counter: active_validators_counter, + active_validators_percentage: + calculate_active_validators_percentage(active_validators_counter, validators_counter) + }) + end + + defp calculate_active_validators_percentage(active_validators_counter, validators_counter) do + if Decimal.compare(validators_counter, Decimal.new(0)) == :gt do + active_validators_counter + |> Decimal.div(validators_counter) + |> Decimal.mult(100) + |> Decimal.to_float() + |> Float.floor(2) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex index b2bbb07d3189..cb040711a856 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.ApiView alias Explorer.Chain + alias Explorer.Chain.SmartContract alias Explorer.SmartContract.Solidity.PublisherWorker, as: SolidityPublisherWorker alias Explorer.SmartContract.Solidity.PublishHelper alias Explorer.SmartContract.Vyper.PublisherWorker, as: VyperPublisherWorker @@ -40,7 +41,8 @@ defmodule BlockScoutWeb.API.V2.VerificationController do vyper_compiler_versions: vyper_compiler_versions, verification_options: verification_options, vyper_evm_versions: CodeCompiler.evm_versions(:vyper), - is_rust_verifier_microservice_enabled: RustVerifierInterface.enabled?() + is_rust_verifier_microservice_enabled: RustVerifierInterface.enabled?(), + license_types: Enum.into(SmartContract.license_types_enum(), %{}) }) end @@ -69,6 +71,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do |> Map.put("name", Map.get(params, "contract_name", "")) |> Map.put("external_libraries", Map.get(params, "libraries", %{})) |> Map.put("is_yul", Map.get(params, "is_yul_contract", false)) + |> Map.put("license_type", Map.get(params, "license_type")) log_sc_verification_started(address_hash_string) Que.add(SolidityPublisherWorker, {"flattened_api_v2", verification_params}) @@ -94,6 +97,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do |> Map.put("autodetect_constructor_args", Map.get(params, "autodetect_constructor_args", true)) |> Map.put("constructor_arguments", Map.get(params, "constructor_args", "")) |> Map.put("name", Map.get(params, "contract_name", "")) + |> Map.put("license_type", Map.get(params, "license_type")) log_sc_verification_started(address_hash_string) Que.add(SolidityPublisherWorker, {"json_api_v2", verification_params, json_input}) @@ -151,6 +155,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do )).() |> Map.put("evm_version", Map.get(params, "evm_version", "default")) |> Map.put("external_libraries", json) + |> Map.put("license_type", Map.get(params, "license_type")) files_array = files @@ -181,6 +186,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do |> Map.put("constructor_arguments", Map.get(params, "constructor_args", "") || "") |> Map.put("name", Map.get(params, "contract_name", "Vyper_contract")) |> Map.put("evm_version", Map.get(params, "evm_version")) + |> Map.put("license_type", Map.get(params, "license_type")) log_sc_verification_started(address_hash_string) Que.add(VyperPublisherWorker, {"vyper_flattened", verification_params}) @@ -208,6 +214,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do } |> Map.put("evm_version", Map.get(params, "evm_version")) |> Map.put("interfaces", interfaces) + |> Map.put("license_type", Map.get(params, "license_type")) files_array = files @@ -234,7 +241,8 @@ defmodule BlockScoutWeb.API.V2.VerificationController do verification_params = %{ "address_hash" => String.downcase(address_hash_string), "compiler_version" => compiler_version, - "input" => json_input + "input" => json_input, + "license_type" => Map.get(params, "license_type") } log_sc_verification_started(address_hash_string) @@ -282,7 +290,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:already_verified, false} <- - {:already_verified, Chain.smart_contract_fully_verified?(address_hash, @api_true)} do + {:already_verified, SmartContract.verified_with_full_match?(address_hash, @api_true)} do :validated end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index fc26823e5211..4282d16d4fbb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -5,6 +5,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] alias Explorer.Chain @@ -20,7 +21,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do conn |> put_status(200) - |> render(:withdrawals, %{withdrawals: withdrawals, next_page_params: next_page_params}) + |> render(:withdrawals, %{withdrawals: withdrawals |> maybe_preload_ens(), next_page_params: next_page_params}) end def withdrawals_counters(conn, _params) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zkevm_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex similarity index 61% rename from apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zkevm_controller.ex rename to apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex index cd45dab110b7..c9bfa544285a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zkevm_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex @@ -1,30 +1,32 @@ -defmodule BlockScoutWeb.API.V2.ZkevmController do +defmodule BlockScoutWeb.API.V2.ZkSyncController do use BlockScoutWeb, :controller import BlockScoutWeb.Chain, only: [ - next_page_params: 3, + next_page_params: 4, paging_options: 1, split_list_by_page: 1 ] - alias Explorer.Chain.Zkevm.Reader + alias Explorer.Chain.ZkSync.{Reader, TransactionBatch} action_fallback(BlockScoutWeb.API.V2.FallbackController) @batch_necessity_by_association %{ - :sequence_transaction => :optional, - :verify_transaction => :optional, + :commit_transaction => :optional, + :prove_transaction => :optional, + :execute_transaction => :optional, :l2_transactions => :optional } @batches_necessity_by_association %{ - :sequence_transaction => :optional, - :verify_transaction => :optional + :commit_transaction => :optional, + :prove_transaction => :optional, + :execute_transaction => :optional } @doc """ - Function to handle GET requests to `/api/v2/zkevm/batches/:batch_number` endpoint. + Function to handle GET requests to `/api/v2/zksync/batches/:batch_number` endpoint. """ @spec batch(Plug.Conn.t(), map()) :: Plug.Conn.t() def batch(conn, %{"batch_number" => batch_number} = _params) do @@ -36,7 +38,7 @@ defmodule BlockScoutWeb.API.V2.ZkevmController do {:ok, batch} -> conn |> put_status(200) - |> render(:zkevm_batch, %{batch: batch}) + |> render(:zksync_batch, %{batch: batch}) {:error, :not_found} = res -> res @@ -44,17 +46,7 @@ defmodule BlockScoutWeb.API.V2.ZkevmController do end @doc """ - Function to handle GET requests to `/api/v2/main-page/zkevm/batches/latest-number` endpoint. - """ - @spec batch_latest_number(Plug.Conn.t(), map()) :: Plug.Conn.t() - def batch_latest_number(conn, _params) do - conn - |> put_status(200) - |> render(:zkevm_batch_latest_number, %{number: batch_latest_number()}) - end - - @doc """ - Function to handle GET requests to `/api/v2/zkevm/batches` endpoint. + Function to handle GET requests to `/api/v2/zksync/batches` endpoint. """ @spec batches(Plug.Conn.t(), map()) :: Plug.Conn.t() def batches(conn, params) do @@ -66,28 +58,34 @@ defmodule BlockScoutWeb.API.V2.ZkevmController do |> Reader.batches() |> split_list_by_page() - next_page_params = next_page_params(next_page, batches, params) + next_page_params = + next_page_params( + next_page, + batches, + params, + fn %TransactionBatch{number: number} -> %{"number" => number} end + ) conn |> put_status(200) - |> render(:zkevm_batches, %{ + |> render(:zksync_batches, %{ batches: batches, next_page_params: next_page_params }) end @doc """ - Function to handle GET requests to `/api/v2/zkevm/batches/count` endpoint. + Function to handle GET requests to `/api/v2/zksync/batches/count` endpoint. """ @spec batches_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def batches_count(conn, _params) do conn |> put_status(200) - |> render(:zkevm_batches_count, %{count: batch_latest_number()}) + |> render(:zksync_batches_count, %{count: Reader.batches_count(api?: true)}) end @doc """ - Function to handle GET requests to `/api/v2/main-page/zkevm/batches/confirmed` endpoint. + Function to handle GET requests to `/api/v2/main-page/zksync/batches/confirmed` endpoint. """ @spec batches_confirmed(Plug.Conn.t(), map()) :: Plug.Conn.t() def batches_confirmed(conn, _params) do @@ -100,7 +98,17 @@ defmodule BlockScoutWeb.API.V2.ZkevmController do conn |> put_status(200) - |> render(:zkevm_batches, %{batches: batches}) + |> render(:zksync_batches, %{batches: batches}) + end + + @doc """ + Function to handle GET requests to `/api/v2/main-page/zksync/batches/latest-number` endpoint. + """ + @spec batch_latest_number(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batch_latest_number(conn, _params) do + conn + |> put_status(200) + |> render(:zksync_batch_latest_number, %{number: batch_latest_number()}) end defp batch_latest_number do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index eabc844c7d01..41bd04b4a536 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -67,19 +67,19 @@ defmodule BlockScoutWeb.ChainController do end def search(conn, %{"q" => query}) do + search_path = + conn + |> search_path(:search_results, q: query) + |> Controller.full_path() + query |> String.trim() |> BlockScoutWeb.Chain.from_param() |> case do {:ok, item} -> - redirect_search_results(conn, item) + redirect_search_results(conn, item, search_path) {:error, :not_found} -> - search_path = - conn - |> search_path(:search_results, q: query) - |> Controller.full_path() - redirect(conn, to: search_path) end end @@ -150,7 +150,7 @@ defmodule BlockScoutWeb.ChainController do end end - defp redirect_search_results(conn, %Address{} = item) do + defp redirect_search_results(conn, %Address{} = item, _search_path) do address_path = conn |> address_path(:show, item) @@ -159,7 +159,7 @@ defmodule BlockScoutWeb.ChainController do redirect(conn, to: address_path) end - defp redirect_search_results(conn, %Block{} = item) do + defp redirect_search_results(conn, %Block{} = item, _search_path) do block_path = conn |> block_path(:show, item) @@ -168,7 +168,7 @@ defmodule BlockScoutWeb.ChainController do redirect(conn, to: block_path) end - defp redirect_search_results(conn, %Transaction{} = item) do + defp redirect_search_results(conn, %Transaction{} = item, _search_path) do transaction_path = conn |> transaction_path(:show, item) @@ -176,4 +176,8 @@ defmodule BlockScoutWeb.ChainController do redirect(conn, to: transaction_path) end + + defp redirect_search_results(conn, _item, search_path) do + redirect(conn, to: search_path) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/csv_export_controller.ex index 1cb332b2a49e..aa04ec19dad2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/csv_export_controller.ex @@ -3,10 +3,11 @@ defmodule BlockScoutWeb.CsvExportController do alias BlockScoutWeb.AccessHelper alias Explorer.Chain + alias Explorer.Chain.Address def index(conn, %{"address" => address_hash_string, "type" => type} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - :ok <- Chain.check_address_exists(address_hash), + :ok <- Address.check_address_exists(address_hash), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), true <- supported_export_type(type), filter_type <- Map.get(params, "filter_type"), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex index f3406fc6cab6..0863579eab95 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.Hash + alias Explorer.Chain.{DenormalizationHelper, Hash} alias Phoenix.View {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) @@ -13,17 +13,22 @@ defmodule BlockScoutWeb.RecentTransactionsController do def index(conn, _params) do if ajax?(conn) do recent_transactions = - Chain.recent_collated_transactions(true, - necessity_by_association: %{ - :block => :required, - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - [created_contract_address: :smart_contract] => :optional, - [from_address: :smart_contract] => :optional, - [to_address: :smart_contract] => :optional - }, - paging_options: %PagingOptions{page_size: 5} + Chain.recent_collated_transactions( + true, + DenormalizationHelper.extend_block_necessity( + [ + necessity_by_association: %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + [created_contract_address: :smart_contract] => :optional, + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional + }, + paging_options: %PagingOptions{page_size: 5} + ], + :required + ) ) transactions = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index 30832eee4aae..6b333af67cf6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -65,7 +65,7 @@ defmodule BlockScoutWeb.SmartContractController do implementation_abi = if contract_type == "proxy" do implementation_address_hash_string - |> Chain.get_implementation_abi() + |> SmartContract.get_smart_contract_abi() |> Poison.encode!() else [] @@ -224,7 +224,7 @@ defmodule BlockScoutWeb.SmartContractController do end defp convert_map_to_array(map) do - if is_turned_out_array?(map) do + if turned_out_array?(map) do map |> Map.values() |> try_to_map_elements() else try_to_map_elements(map) @@ -239,11 +239,11 @@ defmodule BlockScoutWeb.SmartContractController do end end - defp is_turned_out_array?(map) when is_map(map), do: Enum.all?(Map.keys(map), &is_integer?/1) + defp turned_out_array?(map) when is_map(map), do: Enum.all?(Map.keys(map), &integer?/1) - defp is_turned_out_array?(_), do: false + defp turned_out_array?(_), do: false - defp is_integer?(string) when is_binary(string) do + defp integer?(string) when is_binary(string) do case string |> String.trim() |> Integer.parse() do {_, ""} -> true @@ -253,9 +253,9 @@ defmodule BlockScoutWeb.SmartContractController do end end - defp is_integer?(integer) when is_integer(integer), do: true + defp integer?(integer) when is_integer(integer), do: true - defp is_integer?(_), do: false + defp integer?(_), do: false defp write_contract_api_disabled?(action), do: AddressView.contract_interaction_disabled?() && action == "write" end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex index f792297e3a3c..1e9828a4735d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/contract_controller.ex @@ -6,13 +6,13 @@ defmodule BlockScoutWeb.Tokens.ContractController do alias BlockScoutWeb.{AccessHelper, TabHelper} alias Explorer.Chain - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, SmartContract} def index(conn, %{"token_id" => address_hash_string} = params) do options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - :ok <- Chain.check_verified_smart_contract_exists(address_hash), + :ok <- SmartContract.check_verified_smart_contract_exists(address_hash), {:ok, token} <- Chain.token_from_address_hash(address_hash, options), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do %{type: type, action: action} = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex index 2ee82f5253cf..2b2eab99b7ef 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex @@ -12,7 +12,7 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(address_hash), - false <- Chain.is_erc_20_token?(token), + false <- Chain.erc_20_token?(token), {token_id, ""} <- Integer.parse(token_id_str), token_holders <- Chain.fetch_token_holders_from_token_hash_and_token_id(address_hash, token_id, paging_options(params)) do @@ -58,9 +58,9 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), - false <- Chain.is_erc_20_token?(token), + false <- Chain.erc_20_token?(token), {token_id, ""} <- Integer.parse(token_id_str) do - case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do + case Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do {:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token) {:error, :not_found} -> Helper.render(conn, nil, hash, token_id, token) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex index 2f02185d63af..0036a95563ca 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex @@ -9,10 +9,10 @@ defmodule BlockScoutWeb.Tokens.Instance.MetadataController do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), - false <- Chain.is_erc_20_token?(token), + false <- Chain.erc_20_token?(token), {token_id, ""} <- Integer.parse(token_id_str), {:ok, token_instance} <- - Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do + Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do if token_instance.metadata do Helper.render(conn, token_instance, hash, token_id, token) else diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex index 0eaa0115adc8..30a8212a75f9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_str, "type" => "JSON"} = params) do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash), - false <- Chain.is_erc_20_token?(token), + false <- Chain.erc_20_token?(token), {token_id, ""} <- Integer.parse(token_id_str), token_transfers <- Chain.fetch_token_transfers_from_token_hash_and_token_id(hash, token_id, paging_options(params)) do @@ -61,9 +61,9 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), - false <- Chain.is_erc_20_token?(token), + false <- Chain.erc_20_token?(token), {token_id, ""} <- Integer.parse(token_id_str) do - case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, hash) do + case Chain.nft_instance_from_token_id_and_token_address(token_id, hash) do {:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token) {:error, :not_found} -> Helper.render(conn, nil, hash, token_id, token) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex index 65d11fae762b..fd222f9294e6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Tokens.InstanceController do def show(conn, %{"token_id" => token_address_hash, "id" => token_id}) do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash), - false <- Chain.is_erc_20_token?(token) do + false <- Chain.erc_20_token?(token) do token_instance_transfer_path = conn |> token_instance_transfer_path(:index, token_address_hash, token_id) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex index 2d09db084535..1b7f2c5bb560 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex @@ -20,6 +20,7 @@ defmodule BlockScoutWeb.Tokens.InventoryController do unique_token_instances = Chain.address_to_unique_tokens( token.contract_address_hash, + token, unique_tokens_paging_options(params) ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex index a0bae11f5ef8..17ee2823fcd5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.TokensController do import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] alias BlockScoutWeb.{Controller, TokensView} - alias Explorer.Chain + alias Explorer.Chain.Token alias Phoenix.View def index(conn, %{"type" => "JSON"} = params) do @@ -18,7 +18,7 @@ defmodule BlockScoutWeb.TokensController do params |> paging_options() - tokens = Chain.list_top_tokens(filter, paging_params) + tokens = Token.list_top(filter, paging_params) {tokens_page, next_page} = split_list_by_page(tokens) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index 51a75bf85fcb..a31baf1ea923 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -26,6 +26,7 @@ defmodule BlockScoutWeb.TransactionController do alias Explorer.{Chain, Market} alias Explorer.Chain.Cache.Transaction, as: TransactionCache + alias Explorer.Chain.DenormalizationHelper alias Phoenix.View @necessity_by_association %{ @@ -42,7 +43,6 @@ defmodule BlockScoutWeb.TransactionController do @default_options [ necessity_by_association: %{ - :block => :required, [created_contract_address: :names] => :optional, [from_address: :names] => :optional, [to_address: :names] => :optional, @@ -55,6 +55,7 @@ defmodule BlockScoutWeb.TransactionController do def index(conn, %{"type" => "JSON"} = params) do options = @default_options + |> DenormalizationHelper.extend_block_necessity(:required) |> Keyword.merge(paging_options(params)) full_options = @@ -152,10 +153,7 @@ defmodule BlockScoutWeb.TransactionController do :ok <- Chain.check_transaction_exists(transaction_hash) do if Chain.transaction_has_token_transfers?(transaction_hash) do with {:ok, transaction} <- - Chain.hash_to_transaction( - transaction_hash, - necessity_by_association: @necessity_by_association - ), + Chain.hash_to_transaction(transaction_hash, necessity_by_association: @necessity_by_association), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do render( @@ -190,10 +188,7 @@ defmodule BlockScoutWeb.TransactionController do end else with {:ok, transaction} <- - Chain.hash_to_transaction( - transaction_hash, - necessity_by_association: @necessity_by_association - ), + Chain.hash_to_transaction(transaction_hash, necessity_by_association: @necessity_by_association), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do render( diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex index 4c6a017e776d..5ea1e447211a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView, TransactionController} alias Explorer.{Chain, Market} + alias Explorer.Chain.DenormalizationHelper alias Phoenix.View def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do @@ -17,20 +18,19 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do full_options = - Keyword.merge( - [ - necessity_by_association: %{ - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - [transaction: :block] => :optional, - [created_contract_address: :smart_contract] => :optional, - [from_address: :smart_contract] => :optional, - [to_address: :smart_contract] => :optional - } - ], - paging_options(params) - ) + [ + necessity_by_association: %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + [created_contract_address: :smart_contract] => :optional, + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional, + :transaction => :optional + } + ] + |> DenormalizationHelper.extend_transaction_block_necessity(:optional) + |> Keyword.merge(paging_options(params)) internal_transactions_plus_one = Chain.transaction_to_internal_transactions(transaction_hash, full_options) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex index 2059498cce2a..1eec03d72678 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.VerifiedContractsController do alias BlockScoutWeb.{Controller, VerifiedContractsView} alias Explorer.Chain + alias Explorer.Chain.SmartContract alias Phoenix.View @necessity_by_association %{[address: :token] => :optional} @@ -19,7 +20,7 @@ defmodule BlockScoutWeb.VerifiedContractsController do |> Keyword.merge(current_filter(params)) |> Keyword.merge(search_query(params)) - verified_contracts_plus_one = Chain.verified_contracts(full_options) + verified_contracts_plus_one = SmartContract.verified_contracts(full_options) {verified_contracts, next_page} = split_list_by_page(verified_contracts_plus_one) items = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex index 3863fb31a8c6..78c4d225f381 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.VisualizeSol2umlController do # check that contract is verified. partial and twin verification is ok for this case false <- is_nil(address.smart_contract) do sources = - address.smart_contract_additional_sources + address.smart_contract.smart_contract_additional_sources |> Enum.map(fn additional_source -> {additional_source.file_name, additional_source.contract_source_code} end) |> Enum.into(%{}) |> Map.merge(%{ diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 74b1c3cfeeb1..b6be61809eb5 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Etherscan do @moduledoc """ Documentation data for Etherscan-compatible API. """ + alias Explorer.Chain.BridgedToken @account_balance_example_value %{ "status" => "1", @@ -1420,12 +1421,12 @@ defmodule BlockScoutWeb.Etherscan do "A string representing the order by block number direction. Defaults to descending order. Available values: asc, desc" }, %{ - key: "start_block", + key: "startblock", type: "integer", description: "A nonnegative integer that represents the starting block number." }, %{ - key: "end_block", + key: "endblock", type: "integer", description: "A nonnegative integer that represents the ending block number." }, @@ -1513,13 +1514,13 @@ defmodule BlockScoutWeb.Etherscan do "A string representing the order by block number direction. Defaults to ascending order. Available values: asc, desc. WARNING: Only available if 'address' is provided." }, %{ - key: "start_block", + key: "startblock", type: "integer", description: "A nonnegative integer that represents the starting block number. WARNING: Only available if 'address' is provided." }, %{ - key: "end_block", + key: "endblock", type: "integer", description: "A nonnegative integer that represents the ending block number. WARNING: Only available if 'address' is provided." @@ -1588,12 +1589,12 @@ defmodule BlockScoutWeb.Etherscan do "A string representing the order by block number direction. Defaults to ascending order. Available values: asc, desc" }, %{ - key: "start_block", + key: "startblock", type: "integer", description: "A nonnegative integer that represents the starting block number." }, %{ - key: "end_block", + key: "endblock", type: "integer", description: "A nonnegative integer that represents the ending block number." }, @@ -2009,6 +2010,117 @@ defmodule BlockScoutWeb.Etherscan do ] } + if Application.compile_env(:explorer, BridgedToken)[:enabled] do + @success_status_type %{ + type: "status", + enum: ~s(["1"]), + enum_interpretation: %{"1" => "ok"} + } + + @bridged_token_details %{ + name: "Bridged Token Detail", + fields: %{ + foreignChainId: %{ + type: "value", + definition: "Chain ID of the chain where original token exists.", + example: ~s("1") + }, + foreignTokenContractAddressHash: @address_hash_type, + homeContractAddressHash: @address_hash_type, + homeDecimals: @token_decimal_type, + homeHolderCount: %{ + type: "value", + definition: "Token holders count.", + example: ~s("393") + }, + homeName: @token_name_type, + homeSymbol: @token_symbol_type, + homeTotalSupply: %{ + type: "value", + definition: "Total supply of the token on the home side (where token was bridged).", + example: ~s("1484374.775044204093387391") + }, + homeUsdValue: %{ + type: "value", + definition: "Total supply of the token on the home side (where token was bridged) in USD.", + example: ~s("6638727.472651464170990256943") + } + } + } + + @token_bridgedtokenlist_example_value %{ + "status" => "1", + "message" => "OK", + "result" => [ + %{ + "foreignChainId" => "1", + "foreignTokenContractAddressHash" => "0x0ae055097c6d159879521c384f1d2123d1f195e6", + "homeContractAddressHash" => "0xb7d311e2eb55f2f68a9440da38e7989210b9a05e", + "homeDecimals" => "18", + "homeHolderCount" => 393, + "homeName" => "STAKE on xDai", + "homeSymbol" => "STAKE", + "homeTotalSupply" => "1484374.775044204093387391", + "homeUsdValue" => "18807028.39981006586321824397" + }, + %{ + "foreignChainId" => "1", + "foreignTokenContractAddressHash" => "0xf5581dfefd8fb0e4aec526be659cfab1f8c781da", + "homeContractAddressHash" => "0xd057604a14982fe8d88c5fc25aac3267ea142a08", + "homeDecimals" => "18", + "homeHolderCount" => 73, + "homeName" => "HOPR Token on xDai", + "homeSymbol" => "HOPR", + "homeTotalSupply" => "26600449.86076749062791602", + "homeUsdValue" => "6638727.472651464170990256943" + } + ] + } + + @token_bridgedtokenlist_action %{ + name: "bridgedTokenList", + description: "Get bridged tokens list.", + required_params: [], + optional_params: [ + %{ + key: "chainid", + type: "integer", + description: "A nonnegative integer that represents the chain id, where original token exists." + }, + %{ + key: "page", + type: "integer", + description: + "A nonnegative integer that represents the page number to be used for pagination. 'offset' must be provided in conjunction." + }, + %{ + key: "offset", + type: "integer", + description: + "A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction." + } + ], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@token_bridgedtokenlist_example_value), + model: %{ + name: "Result", + fields: %{ + status: @success_status_type, + message: @message_type, + result: %{ + type: "array", + array_type: @bridged_token_details + } + } + } + } + ] + } + end + @stats_tokensupply_action %{ name: "tokensupply", description: @@ -2947,12 +3059,18 @@ defmodule BlockScoutWeb.Etherscan do actions: [@logs_getlogs_action] } + @base_token_actions [ + @token_gettoken_action, + @token_gettokenholders_action + ] + + @token_actions if Application.compile_env(:explorer, BridgedToken)[:enabled], + do: [@token_bridgedtokenlist_action, @base_token_actions], + else: @base_token_actions + @token_module %{ name: "token", - actions: [ - @token_gettoken_action, - @token_gettokenholders_action - ] + actions: @token_actions } @stats_module %{ diff --git a/apps/block_scout_web/lib/block_scout_web/main_page_realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/main_page_realtime_event_handler.ex new file mode 100644 index 000000000000..8c34538f4751 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/main_page_realtime_event_handler.ex @@ -0,0 +1,29 @@ +defmodule BlockScoutWeb.MainPageRealtimeEventHandler do + @moduledoc """ + Subscribing process for main page broadcast events from realtime. + """ + + use GenServer + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Events.Subscriber + alias Explorer.Counters.Helper + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @impl true + def init([]) do + Helper.create_cache_table(:last_broadcasted_block) + Subscriber.to(:blocks, :realtime) + Subscriber.to(:transactions, :realtime) + {:ok, []} + end + + @impl true + def handle_info(event, state) do + Notifier.handle_event(event) + {:noreply, state} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex new file mode 100644 index 000000000000..5bb328ad8f6a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -0,0 +1,332 @@ +defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do + @moduledoc """ + Module to interact with Transaction Interpretation Service + """ + + alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView} + alias Ecto.Association.NotLoaded + alias Explorer.Chain + alias Explorer.Chain.{Data, Log, TokenTransfer, Transaction} + alias HTTPoison.Response + + import Explorer.Utility.Microservice, only: [base_url: 2, check_enabled: 2] + + require Logger + + @post_timeout :timer.minutes(5) + @request_error_msg "Error while sending request to Transaction Interpretation Service" + @api_true api?: true + @items_limit 50 + + @doc """ + Interpret transaction or user operation + """ + @spec interpret(Transaction.t() | map(), (Transaction.t() -> any()) | (map() -> any())) :: + {{:error, :disabled | binary()}, integer()} + | {:error, Jason.DecodeError.t()} + | {:ok, any()} + def interpret(transaction_or_map, request_builder \\ &prepare_request_body/1) do + if enabled?() do + url = interpret_url() + + body = request_builder.(transaction_or_map) + + http_post_request(url, body) + else + {{:error, :disabled}, 403} + end + end + + @doc """ + Interpret user operation + """ + @spec interpret_user_operation(map()) :: + {{:error, :disabled | binary()}, integer()} + | {:error, Jason.DecodeError.t()} + | {:ok, any()} + def interpret_user_operation(user_operation) do + interpret(user_operation, &prepare_request_body_from_user_op/1) + end + + @doc """ + Build the request body as for the tx interpreter POST request. + """ + @spec get_request_body(Transaction.t()) :: map() + def get_request_body(transaction) do + prepare_request_body(transaction) + end + + @doc """ + Build the request body as for the tx interpreter POST request. + """ + @spec get_user_op_request_body(map()) :: map() + def get_user_op_request_body(user_op) do + prepare_request_body_from_user_op(user_op) + end + + defp http_post_request(url, body) do + headers = [{"Content-Type", "application/json"}] + + case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %Response{body: body, status_code: 200}} -> + body |> Jason.decode() |> preload_template_variables() + + error -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to microservice url: #{url}, body: #{inspect(body, limit: :infinity, printable_limit: :infinity)}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {{:error, @request_error_msg}, http_response_code(error)} + end + end + + defp http_response_code({:ok, %Response{status_code: status_code}}), do: status_code + defp http_response_code(_), do: 500 + + def enabled?, do: check_enabled(:block_scout_web, __MODULE__) == :ok + + defp interpret_url do + base_url(:block_scout_web, __MODULE__) <> "/transactions/summary" + end + + defp prepare_request_body(transaction) do + transaction = + Chain.select_repo(@api_true).preload(transaction, [ + :transaction_actions, + to_address: [:names, :smart_contract], + from_address: [:names, :smart_contract], + created_contract_address: [:names, :token, :smart_contract] + ]) + + skip_sig_provider? = false + {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true) + + decoded_input_data = decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input() + + %{ + data: %{ + to: Helper.address_with_info(nil, transaction.to_address, transaction.to_address_hash, true), + from: + Helper.address_with_info( + nil, + transaction.from_address, + transaction.from_address_hash, + true + ), + hash: transaction.hash, + type: transaction.type, + value: transaction.value, + method: TransactionView.method_name(transaction, TransactionView.format_decoded_input(decoded_input)), + status: transaction.status, + actions: TransactionView.transaction_actions(transaction.transaction_actions), + tx_types: TransactionView.tx_types(transaction), + raw_input: transaction.input, + decoded_input: decoded_input_data, + token_transfers: prepare_token_transfers(transaction, decoded_input) + }, + logs_data: %{items: prepare_logs(transaction)} + } + end + + defp prepare_token_transfers(transaction, decoded_input) do + full_options = + [ + necessity_by_association: %{ + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional + } + ] + |> Keyword.merge(@api_true) + + transaction.hash + |> Chain.transaction_to_token_transfers(full_options) + |> Chain.flat_1155_batch_token_transfers() + |> Enum.take(@items_limit) + |> Enum.map(&TransactionView.prepare_token_transfer(&1, nil, decoded_input)) + end + + defp user_op_to_logs_and_token_transfers(user_op, decoded_input) do + log_options = + [ + necessity_by_association: %{ + [address: :names] => :optional, + [address: :smart_contract] => :optional, + address: :optional + }, + limit: @items_limit + ] + |> Keyword.merge(@api_true) + + logs = Log.user_op_to_logs(user_op, log_options) + + decoded_logs = TransactionView.decode_logs(logs, false) + + prepared_logs = + logs + |> Enum.zip(decoded_logs) + |> Enum.map(fn {log, decoded_log} -> + TransactionView.prepare_log(log, user_op["transaction_hash"], decoded_log, true) + end) + + token_transfer_options = + [ + necessity_by_association: %{ + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + :token => :optional + } + ] + |> Keyword.merge(@api_true) + + prepared_token_transfers = + logs + |> TokenTransfer.logs_to_token_transfers(token_transfer_options) + |> Chain.flat_1155_batch_token_transfers() + |> Enum.take(@items_limit) + |> Enum.map(&TransactionView.prepare_token_transfer(&1, nil, decoded_input)) + + {prepared_logs, prepared_token_transfers} + end + + defp prepare_logs(transaction) do + full_options = + [ + necessity_by_association: %{ + [address: :names] => :optional, + [address: :smart_contract] => :optional, + address: :optional + } + ] + |> Keyword.merge(@api_true) + + logs = + transaction.hash + |> Chain.transaction_to_logs(full_options) + |> Enum.take(@items_limit) + + decoded_logs = TransactionView.decode_logs(logs, false) + + logs + |> Enum.zip(decoded_logs) + |> Enum.map(fn {log, decoded_log} -> TransactionView.prepare_log(log, transaction.hash, decoded_log, true) end) + end + + defp preload_template_variables({:ok, %{"success" => true, "data" => %{"summaries" => summaries} = data}}) do + summaries_updated = + Enum.map(summaries, fn %{"summary_template_variables" => summary_template_variables} = summary -> + summary_template_variables_preloaded = + Enum.reduce(summary_template_variables, %{}, fn {key, value}, acc -> + Map.put(acc, key, preload_template_variable(value)) + end) + + Map.put(summary, "summary_template_variables", summary_template_variables_preloaded) + end) + + {:ok, %{"success" => true, "data" => Map.put(data, "summaries", summaries_updated)}} + end + + defp preload_template_variables(error), do: error + + defp preload_template_variable(%{"type" => "token", "value" => %{"address" => address_hash_string} = value}), + do: %{ + "type" => "token", + "value" => address_hash_string |> Chain.token_from_address_hash(@api_true) |> token_from_db() |> Map.merge(value) + } + + defp preload_template_variable(%{"type" => "address", "value" => %{"hash" => address_hash_string} = value}), + do: %{ + "type" => "address", + "value" => + address_hash_string + |> Chain.hash_to_address( + [ + necessity_by_association: %{ + :names => :optional, + :smart_contract => :optional + }, + api?: true + ], + false + ) + |> address_from_db() + |> Map.merge(value) + } + + defp preload_template_variable(other), do: other + + defp token_from_db({:error, _}), do: %{} + defp token_from_db({:ok, token}), do: TokenView.render("token.json", %{token: token}) + + defp address_from_db({:error, _}), do: %{} + + defp address_from_db({:ok, address}), + do: + Helper.address_with_info( + nil, + address, + address.hash, + true + ) + + defp prepare_request_body_from_user_op(user_op) do + {mock_tx, decoded_input, decoded_input_json} = decode_user_op_calldata(user_op) + + {prepared_logs, prepared_token_transfers} = user_op_to_logs_and_token_transfers(user_op, decoded_input) + + {:ok, from_address_hash} = Chain.string_to_address_hash(user_op["sender"]) + + from_address = Chain.hash_to_address(from_address_hash, []) + + %{ + data: %{ + to: nil, + from: Helper.address_with_info(nil, from_address, from_address_hash, true), + hash: user_op["hash"], + type: 0, + value: "0", + method: TransactionView.method_name(mock_tx, TransactionView.format_decoded_input(decoded_input), true), + status: user_op["status"], + actions: [], + tx_types: [], + raw_input: user_op["call_data"], + decoded_input: decoded_input_json, + token_transfers: prepared_token_transfers + }, + logs_data: %{items: prepared_logs} + } + end + + @doc """ + Decodes user_op["call_data"] and return {mock_tx, decoded_input, decoded_input_json} + """ + @spec decode_user_op_calldata(map()) :: {Transaction.t(), tuple(), map()} + def decode_user_op_calldata(user_op) do + {:ok, input} = Data.cast(user_op["call_data"]) + + {:ok, op_hash} = Chain.string_to_transaction_hash(user_op["hash"]) + + mock_tx = %Transaction{ + to_address: %NotLoaded{}, + input: input, + hash: op_hash + } + + skip_sig_provider? = false + + {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true) + + decoded_input_json = decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input() + {mock_tx, decoded_input, decoded_input_json} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 3971a5463a64..e37d94adfab9 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do alias Explorer.Chain.Transaction.StateChange alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{Block, Transaction, Wei} + alias Explorer.Chain.{Block, BlockNumberHelper, Transaction, Wei} alias Explorer.Chain.Cache.StateChanges alias Indexer.Fetcher.{CoinBalanceOnDemand, TokenBalanceOnDemand} @@ -73,9 +73,11 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do api?: Keyword.get(options, :api?, false) ) - from_before_block = coin_balance(transaction.from_address_hash, block.number - 1, options) - to_before_block = coin_balance(transaction.to_address_hash, block.number - 1, options) - miner_before_block = coin_balance(block.miner_hash, block.number - 1, options) + previous_block_number = BlockNumberHelper.previous_block_number(block.number) + + from_before_block = coin_balance(transaction.from_address_hash, previous_block_number, options) + to_before_block = coin_balance(transaction.to_address_hash, previous_block_number, options) + miner_before_block = coin_balance(block.miner_hash, previous_block_number, options) {from_before_tx, to_before_tx, miner_before_tx} = StateChange.coin_balances_before(transaction, block_txs, from_before_block, to_before_block, miner_before_block) @@ -112,7 +114,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do token_ids = if token.type == "ERC-1155" do - token_transfer.token_ids || [token_transfer.token_id] + token_transfer.token_ids else [nil] end @@ -146,7 +148,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do from = transfer.from_address to = transfer.to_address token_hash = transfer.token_contract_address_hash - prev_block = transfer.block_number - 1 + prev_block = BlockNumberHelper.previous_block_number(transfer.block_number) balances |> case do diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index cb1cdf70be02..ca7027ae89aa 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -20,7 +20,7 @@ defmodule BlockScoutWeb.Notifier do alias Explorer.{Chain, Market, Repo} alias Explorer.Chain.Address.Counters - alias Explorer.Chain.{Address, InternalTransaction, Transaction} + alias Explorer.Chain.{Address, BlockNumberHelper, DenormalizationHelper, InternalTransaction, Transaction} alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Counters.{AverageBlockTime, Helper} @@ -171,7 +171,9 @@ defmodule BlockScoutWeb.Notifier do all_token_transfers |> Enum.map( &(&1 - |> Repo.preload([:from_address, :to_address, :token, transaction: :block])) + |> Repo.preload( + DenormalizationHelper.extend_transaction_preload([:from_address, :to_address, :token, :transaction]) + )) ) transfers_by_token = Enum.group_by(all_token_transfers_full, fn tt -> to_string(tt.token_contract_address_hash) end) @@ -191,13 +193,11 @@ defmodule BlockScoutWeb.Notifier do end def handle_event({:chain_event, :transactions, :realtime, transactions}) do - preloads = [:block, created_contract_address: :names, from_address: :names, to_address: :names] + base_preloads = [:block, created_contract_address: :names, from_address: :names, to_address: :names] + preloads = if API_V2.enabled?(), do: [:token_transfers | base_preloads], else: base_preloads transactions - |> Enum.map( - &(&1 - |> Repo.preload(if API_V2.enabled?(), do: [:token_transfers | preloads], else: preloads)) - ) + |> Repo.preload(preloads) |> broadcast_transactions_websocket_v2() |> Enum.map(fn tx -> # Disable parsing of token transfers from websocket for transaction tab because we display token transfers at a separate tab @@ -232,9 +232,20 @@ defmodule BlockScoutWeb.Notifier do Endpoint.broadcast("addresses:#{to_string(address_hash)}", "changed_bytecode", %{}) end - def handle_event({:chain_event, :smart_contract_was_verified, :on_demand, [address_hash]}) do - log_broadcast_smart_contract_was_verified(address_hash) - Endpoint.broadcast("addresses:#{to_string(address_hash)}", "smart_contract_was_verified", %{}) + def handle_event({:chain_event, :optimism_deposits, :realtime, deposits}) do + broadcast_optimism_deposits(deposits, "optimism_deposits:new_deposits", "deposits") + end + + def handle_event({:chain_event, :smart_contract_was_verified = event, :on_demand, [address_hash]}) do + broadcast_automatic_verification_events(event, address_hash) + end + + def handle_event({:chain_event, :smart_contract_was_not_verified = event, :on_demand, [address_hash]}) do + broadcast_automatic_verification_events(event, address_hash) + end + + def handle_event({:chain_event, :eth_bytecode_db_lookup_started = event, :on_demand, [address_hash]}) do + broadcast_automatic_verification_events(event, address_hash) end def handle_event({:chain_event, :address_current_token_balances, :on_demand, address_current_token_balances}) do @@ -298,12 +309,13 @@ defmodule BlockScoutWeb.Notifier do defp broadcast_latest_block?(block, last_broadcasted_block_number) do cond do - last_broadcasted_block_number == 0 || last_broadcasted_block_number == block.number - 1 || + last_broadcasted_block_number == 0 || + last_broadcasted_block_number == BlockNumberHelper.previous_block_number(block.number) || last_broadcasted_block_number < block.number - 4 -> broadcast_block(block) :ets.insert(:last_broadcasted_block, {:number, block.number}) - last_broadcasted_block_number > block.number - 1 -> + last_broadcasted_block_number > BlockNumberHelper.previous_block_number(block.number) -> broadcast_block(block) true -> @@ -317,7 +329,7 @@ defmodule BlockScoutWeb.Notifier do :timer.sleep(@check_broadcast_sequence_period) last_broadcasted_block_number = Helper.fetch_from_cache(:number, :last_broadcasted_block) - if last_broadcasted_block_number == block.number - 1 do + if last_broadcasted_block_number == BlockNumberHelper.previous_block_number(block.number) do broadcast_block(block) :ets.insert(:last_broadcasted_block, {:number, block.number}) else @@ -392,6 +404,10 @@ defmodule BlockScoutWeb.Notifier do end end + defp broadcast_optimism_deposits(deposits, deposit_channel, event) do + Endpoint.broadcast(deposit_channel, event, %{deposits: deposits}) + end + defp broadcast_transactions_websocket_v2(transactions) do pending_transactions = Enum.filter(transactions, fn @@ -505,7 +521,12 @@ defmodule BlockScoutWeb.Notifier do Logger.info("Broadcast smart-contract #{address_hash} verification results") end - defp log_broadcast_smart_contract_was_verified(address_hash) do - Logger.info("Broadcast smart-contract #{address_hash} was verified") + defp log_broadcast_smart_contract_event(address_hash, event) do + Logger.info("Broadcast smart-contract #{address_hash}: #{event}") + end + + defp broadcast_automatic_verification_events(event, address_hash) do + log_broadcast_smart_contract_event(address_hash, event) + Endpoint.broadcast("addresses:#{to_string(address_hash)}", to_string(event), %{}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 860a8f7d4a15..0626de4025fe 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -3,13 +3,41 @@ defmodule BlockScoutWeb.PagingHelper do Helper for fetching filters and other url query parameters """ import Explorer.Chain, only: [string_to_transaction_hash: 1] - alias Explorer.PagingOptions + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + alias Explorer.Chain.Transaction + alias Explorer.{Helper, PagingOptions, SortingHelper} @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @allowed_filter_labels ["validated", "pending"] - @allowed_type_labels ["coin_transfer", "contract_call", "contract_creation", "token_transfer", "token_creation"] - @allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155"] + + case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> + @allowed_type_labels [ + "coin_transfer", + "contract_call", + "contract_creation", + "token_transfer", + "token_creation", + "blob_transaction" + ] + + _ -> + @allowed_type_labels [ + "coin_transfer", + "contract_call", + "contract_creation", + "token_transfer", + "token_creation" + ] + end + + @allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] + @allowed_nft_type_labels ["ERC-721", "ERC-1155", "ERC-404"] + @allowed_chain_id [1, 56, 99] + @allowed_stability_validators_states ["active", "probation", "inactive"] + + def allowed_stability_validators_states, do: @allowed_stability_validators_states def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do with {block_number, ""} <- Integer.parse(block_number_string), @@ -33,14 +61,38 @@ defmodule BlockScoutWeb.PagingHelper do def paging_options(_params, _filter), do: [paging_options: @default_paging_options] + @spec stability_validators_state_options(map()) :: [{:state, list()}, ...] + def stability_validators_state_options(%{"state_filter" => state}) do + [state: filters_to_list(state, @allowed_stability_validators_states, :downcase)] + end + + def stability_validators_state_options(_), do: [state: []] + + @spec token_transfers_types_options(map()) :: [{:token_type, list}] def token_transfers_types_options(%{"type" => filters}) do [ - token_type: filters |> String.upcase() |> parse_filter(@allowed_token_transfer_type_labels) + token_type: filters_to_list(filters, @allowed_token_transfer_type_labels) ] end def token_transfers_types_options(_), do: [token_type: []] + @doc """ + Parse 'type' query parameter from request option map + """ + @spec nft_types_options(map()) :: [{:token_type, list}] + def nft_types_options(%{"type" => filters}) do + [ + token_type: filters_to_list(filters, @allowed_nft_type_labels) + ] + end + + def nft_types_options(_), do: [token_type: []] + + defp filters_to_list(filters, allowed, variant \\ :upcase) + defp filters_to_list(filters, allowed, :downcase), do: filters |> String.downcase() |> parse_filter(allowed) + defp filters_to_list(filters, allowed, :upcase), do: filters |> String.upcase() |> parse_filter(allowed) + # sobelow_skip ["DOS.StringToAtom"] def filter_options(%{"filter" => filter}, fallback) do filter = filter |> parse_filter(@allowed_filter_labels) |> Enum.map(&String.to_atom/1) @@ -49,6 +101,19 @@ defmodule BlockScoutWeb.PagingHelper do def filter_options(_params, fallback), do: [fallback] + def chain_ids_filter_options(%{"chain_ids" => chain_id}) do + [ + chain_ids: + chain_id + |> String.split(",") + |> Enum.uniq() + |> Enum.map(&Helper.parse_integer/1) + |> Enum.filter(&Enum.member?(@allowed_chain_id, &1)) + ] + end + + def chain_ids_filter_options(_), do: [chain_id: []] + # sobelow_skip ["DOS.StringToAtom"] def type_filter_options(%{"type" => type}) do [type: type |> parse_filter(@allowed_type_labels) |> Enum.map(&String.to_atom/1)] @@ -136,7 +201,8 @@ defmodule BlockScoutWeb.PagingHelper do "filter", "q", "sort", - "order" + "order", + "state_filter" ]) end @@ -170,6 +236,7 @@ defmodule BlockScoutWeb.PagingHelper do def search_query(_), do: [] + @spec tokens_sorting(%{required(String.t()) => String.t()}) :: [{:sorting, SortingHelper.sorting_params()}] def tokens_sorting(%{"sort" => sort_field, "order" => order}) do [sorting: do_tokens_sorting(sort_field, order)] end @@ -183,4 +250,57 @@ defmodule BlockScoutWeb.PagingHelper do defp do_tokens_sorting("circulating_market_cap", "asc"), do: [asc_nulls_first: :circulating_market_cap] defp do_tokens_sorting("circulating_market_cap", "desc"), do: [desc_nulls_last: :circulating_market_cap] defp do_tokens_sorting(_, _), do: [] + + @spec smart_contracts_sorting(%{required(String.t()) => String.t()}) :: [{:sorting, SortingHelper.sorting_params()}] + def smart_contracts_sorting(%{"sort" => sort_field, "order" => order}) do + [sorting: do_smart_contracts_sorting(sort_field, order)] + end + + def smart_contracts_sorting(_), do: [] + + defp do_smart_contracts_sorting("balance", "asc"), do: [{:asc_nulls_first, :fetched_coin_balance, :address}] + defp do_smart_contracts_sorting("balance", "desc"), do: [{:desc_nulls_last, :fetched_coin_balance, :address}] + defp do_smart_contracts_sorting("txs_count", "asc"), do: [{:asc_nulls_first, :transactions_count, :address}] + defp do_smart_contracts_sorting("txs_count", "desc"), do: [{:desc_nulls_last, :transactions_count, :address}] + defp do_smart_contracts_sorting(_, _), do: [] + + @spec address_transactions_sorting(%{required(String.t()) => String.t()}) :: [ + {:sorting, SortingHelper.sorting_params()} + ] + def address_transactions_sorting(%{"sort" => sort_field, "order" => order}) do + [sorting: do_address_transaction_sorting(sort_field, order)] + end + + def address_transactions_sorting(_), do: [] + + defp do_address_transaction_sorting("value", "asc"), do: [asc: :value] + defp do_address_transaction_sorting("value", "desc"), do: [desc: :value] + defp do_address_transaction_sorting("fee", "asc"), do: [{:dynamic, :fee, :asc_nulls_first, Transaction.dynamic_fee()}] + + defp do_address_transaction_sorting("fee", "desc"), + do: [{:dynamic, :fee, :desc_nulls_last, Transaction.dynamic_fee()}] + + defp do_address_transaction_sorting(_, _), do: [] + + @spec validators_stability_sorting(%{required(String.t()) => String.t()}) :: [ + {:sorting, SortingHelper.sorting_params()} + ] + def validators_stability_sorting(%{"sort" => sort_field, "order" => order}) do + [sorting: do_validators_stability_sorting(sort_field, order)] + end + + def validators_stability_sorting(_), do: [] + + defp do_validators_stability_sorting("state", "asc"), do: [asc_nulls_first: :state] + defp do_validators_stability_sorting("state", "desc"), do: [desc_nulls_last: :state] + defp do_validators_stability_sorting("address_hash", "asc"), do: [asc_nulls_first: :address_hash] + defp do_validators_stability_sorting("address_hash", "desc"), do: [desc_nulls_last: :address_hash] + + defp do_validators_stability_sorting("blocks_validated", "asc"), + do: [{:dynamic, :blocks_validated, :asc_nulls_first, ValidatorStability.dynamic_validated_blocks()}] + + defp do_validators_stability_sorting("blocks_validated", "desc"), + do: [{:dynamic, :blocks_validated, :desc_nulls_last, ValidatorStability.dynamic_validated_blocks()}] + + defp do_validators_stability_sorting(_, _), do: [] end diff --git a/apps/block_scout_web/lib/block_scout_web/plug/logger.ex b/apps/block_scout_web/lib/block_scout_web/plug/logger.ex index 883dd95ef3f8..b61c9cafe4c6 100644 --- a/apps/block_scout_web/lib/block_scout_web/plug/logger.ex +++ b/apps/block_scout_web/lib/block_scout_web/plug/logger.ex @@ -19,53 +19,40 @@ defmodule BlockScoutWeb.Plug.Logger do @impl true def call(conn, opts) do level = Keyword.get(opts, :log, :info) - application = Keyword.get(opts, :application, :block_scout_web) - - log(application, conn, level, opts) start = System.monotonic_time() Conn.register_before_send(conn, fn conn -> + stop = System.monotonic_time() + diff = System.convert_time_unit(stop - start, :native, :microsecond) + status = Integer.to_string(conn.status) + Logger.log( level, fn -> - stop = System.monotonic_time() - diff = System.convert_time_unit(stop - start, :native, :microsecond) - status = Integer.to_string(conn.status) - - [connection_type(conn), ?\s, status, " in ", formatted_diff(diff)] + [connection_type(conn), ?\s, status, " in ", formatted_diff(diff), " on ", conn.method, ?\s, endpoint(conn)] end, - opts + Keyword.merge( + [duration: diff, status: status, unit: "microsecond", endpoint: endpoint(conn), method: conn.method], + opts + ) ) conn end) end - defp log(:api, conn, level, opts) do - endpoint = - if conn.query_string do - "#{conn.request_path}?#{conn.query_string}" - else - conn.request_path - end - - Logger.log(level, endpoint, opts) - end - - defp log(_application, conn, level, opts) do - Logger.log( - level, - fn -> - [conn.method, ?\s, conn.request_path] - end, - opts - ) - end - defp formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] defp formatted_diff(diff), do: [Integer.to_string(diff), "µs"] defp connection_type(%{state: :set_chunked}), do: "Chunked" defp connection_type(_), do: "Sent" + + defp endpoint(conn) do + if conn.query_string do + "#{conn.request_path}?#{conn.query_string}" + else + conn.request_path + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index e7b590876380..b08f50e48b3e 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -7,7 +7,6 @@ defmodule BlockScoutWeb.RealtimeEventHandler do alias BlockScoutWeb.Notifier alias Explorer.Chain.Events.Subscriber - alias Explorer.Counters.Helper def start_link(_) do GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -15,23 +14,20 @@ defmodule BlockScoutWeb.RealtimeEventHandler do @impl true def init([]) do - Helper.create_cache_table(:last_broadcasted_block) Subscriber.to(:address_coin_balances, :realtime) Subscriber.to(:addresses, :realtime) Subscriber.to(:block_rewards, :realtime) - Subscriber.to(:blocks, :realtime) Subscriber.to(:internal_transactions, :realtime) Subscriber.to(:internal_transactions, :on_demand) + Subscriber.to(:optimism_deposits, :realtime) Subscriber.to(:token_transfers, :realtime) - Subscriber.to(:transactions, :realtime) Subscriber.to(:addresses, :on_demand) Subscriber.to(:address_coin_balances, :on_demand) Subscriber.to(:address_current_token_balances, :on_demand) Subscriber.to(:address_token_balances, :on_demand) - Subscriber.to(:contract_verification_result, :on_demand) Subscriber.to(:token_total_supply, :on_demand) Subscriber.to(:changed_bytecode, :on_demand) - Subscriber.to(:smart_contract_was_verified, :on_demand) + Subscriber.to(:eth_bytecode_db_lookup_started, :on_demand) Subscriber.to(:zkevm_confirmed_batches, :realtime) # Does not come from the indexer Subscriber.to(:exchange_rate) diff --git a/apps/block_scout_web/lib/block_scout_web/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/schema/types.ex index 99f47a29163b..d81f6eca5137 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/types.ex @@ -130,7 +130,6 @@ defmodule BlockScoutWeb.Schema.Types do field(:amounts, list_of(:decimal)) field(:block_number, :integer) field(:log_index, :integer) - field(:token_id, :decimal) field(:token_ids, list_of(:decimal)) field(:from_address_hash, :address_hash) field(:to_address_hash, :address_hash) diff --git a/apps/block_scout_web/lib/block_scout_web/smart_contract_realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/smart_contract_realtime_event_handler.ex new file mode 100644 index 000000000000..e94e8a3bb9bb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/smart_contract_realtime_event_handler.ex @@ -0,0 +1,28 @@ +defmodule BlockScoutWeb.SmartContractRealtimeEventHandler do + @moduledoc """ + Subscribing process for smart contract verification related broadcast events from realtime. + """ + + use GenServer + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Events.Subscriber + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @impl true + def init([]) do + Subscriber.to(:contract_verification_result, :on_demand) + Subscriber.to(:smart_contract_was_verified, :on_demand) + Subscriber.to(:smart_contract_was_not_verified, :on_demand) + {:ok, []} + end + + @impl true + def handle_info(event, state) do + Notifier.handle_event(event) + {:noreply, state} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex index d3f1d5e162f7..aad961f117f5 100644 --- a/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex @@ -27,6 +27,9 @@ defmodule BlockScoutWeb.SmartContractsApiV2Router do get("/:address_hash/methods-read-proxy", V2.SmartContractController, :methods_read_proxy) get("/:address_hash/methods-write-proxy", V2.SmartContractController, :methods_write_proxy) post("/:address_hash/query-read-method", V2.SmartContractController, :query_read_method) + get("/:address_hash/solidityscan-report", V2.SmartContractController, :solidityscan_report) + post("/:address_hash/audit-reports", V2.SmartContractController, :audit_report_submission) + get("/:address_hash/audit-reports", V2.SmartContractController, :audit_reports_list) get("/verification/config", V2.VerificationController, :config) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex index 275a8f058dc1..d28be9224205 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex @@ -1,7 +1,7 @@ <%= if @address do %> <%= if assigns[:show_full_hash] do %> <%= if name = if assigns[:ignore_implementation_name], do: primary_name(@address), else: implementation_name(@address) || primary_name(@address) do %> - <%= name %> | + <%= name %> | <% end %> <%= link to: address_path(BlockScoutWeb.Endpoint, :show, @address), "data-test": "address_hash_link", class: assigns[:class] do %> <%= @address %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex index 368aa9f91e75..04cefd6cde58 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex @@ -28,7 +28,7 @@ <%= @tx_count %> - <%= gettext "Transactions sent" %> + <%= gettext "Transactions" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index 2b8ba6e6aa23..005508956e65 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -62,7 +62,7 @@ @address.hash), "data-test": "token_hash_link" - ) + ) %> @@ -137,7 +137,7 @@ (if name, do: name <> " | " <> implementation_address, else: implementation_address), to: address_path(@conn, :show, implementation_address), class: "contract-address" - ) + ) %> @@ -160,7 +160,7 @@ data-placement="top" data-toggle="tooltip" data-html="true" - title='<%= "@ " <> usd_value <> "/" <> Explorer.coin_name() %>' + title='<%= "@ " <> usd_value <> "/" <> Explorer.coin_name() %>' > ) @@ -288,4 +288,4 @@ <%= render BlockScoutWeb.CommonComponentsView, "_modal_qr_code.html", qr_code: qr_code(@address), title: @address %> -<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> +<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex index 77c96739cb5d..4a052b2fa539 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex @@ -1,10 +1,9 @@ <% contract_creation_code = contract_creation_code(@address) %> -<% minimal_proxy_template = Chain.get_minimal_proxy_template(@address.hash) %> -<% metadata_for_verification = minimal_proxy_template || Chain.get_address_verified_twin_contract(@address.hash).verified_contract %> +<% minimal_proxy_template = EIP1167.get_implementation_address(@address.hash) %> +<% metadata_for_verification = minimal_proxy_template || SmartContract.get_address_verified_twin_contract(@address.hash).verified_contract %> <% smart_contract_verified = BlockScoutWeb.AddressView.smart_contract_verified?(@address) %> -<% additional_sources_from_twin = Chain.get_address_verified_twin_contract(@address.hash).additional_sources %> -<% fully_verified = Chain.smart_contract_fully_verified?(@address.hash)%> -<% additional_sources = if smart_contract_verified, do: @address.smart_contract_additional_sources, else: additional_sources_from_twin %> +<% fully_verified = SmartContract.verified_with_full_match?(@address.hash)%> +<% additional_sources = BlockScoutWeb.API.V2.SmartContractView.additional_sources(@address.smart_contract, smart_contract_verified, minimal_proxy_template, SmartContract.get_address_verified_twin_contract(@address.hash)) %> <% visualize_sol2uml_enabled = Explorer.Visualize.Sol2uml.enabled?() %>
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> @@ -43,16 +42,16 @@ <% end %> <%= if smart_contract_verified || (!smart_contract_verified && metadata_for_verification) do %> <% target_contract = if smart_contract_verified, do: @address.smart_contract, else: metadata_for_verification %> - <%= if @address.smart_contract.verified_via_sourcify && @address.smart_contract.partially_verified && smart_contract_verified do %> + <%= if @address.smart_contract && @address.smart_contract.verified_via_sourcify && @address.smart_contract.partially_verified && smart_contract_verified do %>
<%= gettext("This contract has been partially verified via Sourcify.") %> <% else %> - <%= if @address.smart_contract.verified_via_sourcify && smart_contract_verified do %> + <%= if @address.smart_contract && @address.smart_contract.verified_via_sourcify && smart_contract_verified do %>
<%= gettext("This contract has been verified via Sourcify.") %> <% end %> <% end %> - <%= if @address.smart_contract.verified_via_sourcify && smart_contract_verified do %> + <%= if @address.smart_contract && @address.smart_contract.verified_via_sourcify && smart_contract_verified do %> target="_blank"> View contract in Sourcify repository <%= render BlockScoutWeb.IconsView, "_external_link.html" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex index 383aa4584d4c..b0a22d4bace6 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex @@ -1,4 +1,4 @@ -<% metadata_for_verification = if assigns[:retrying], do: nil, else: Chain.get_address_verified_twin_contract(@address_hash).verified_contract %> +<% metadata_for_verification = if assigns[:retrying], do: nil, else: SmartContract.get_address_verified_twin_contract(@address_hash).verified_contract %> <% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %> <% fetch_constructor_arguments_automatically = if metadata_for_verification, do: true, else: changeset.changes[:autodetect_constructor_args] || true %> <% display_constructor_arguments_text_area = if fetch_constructor_arguments_automatically, do: "none", else: "block" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex index a3b772456f61..938631b3e1aa 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex @@ -1,4 +1,4 @@ -<% metadata_for_verification = if assigns[:retrying], do: nil, else: Chain.get_address_verified_twin_contract(@address_hash).verified_contract %> +<% metadata_for_verification = if assigns[:retrying], do: nil, else: SmartContract.get_address_verified_twin_contract(@address_hash).verified_contract %> <% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %>
<%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex index 34b8d06cfe9d..a1fb8db78646 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex @@ -1,4 +1,4 @@ -<% metadata_for_verification = Chain.get_address_verified_twin_contract(@address_hash).verified_contract %> +<% metadata_for_verification = SmartContract.get_address_verified_twin_contract(@address_hash).verified_contract %> <% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %> <% fetch_constructor_arguments_automatically = if metadata_for_verification, do: true, else: changeset.changes[:autodetect_constructor_args] || true %> <% display_constructor_arguments_text_area = if fetch_constructor_arguments_automatically, do: "none", else: "block" %> @@ -10,7 +10,7 @@ <%= form_for changeset, address_contract_verification_path(@conn, :create), [id: "standard-json-dropzone-form"], - fn f -> %> + fn f -> %> <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_name_field.html", f: f, tooltip: "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name. Also contract name could be: path/to/file.sol:MyContract" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex index 4b07896c63a6..a98282193b15 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex @@ -1,4 +1,4 @@ -<% metadata_for_verification = Chain.get_address_verified_twin_contract(@address_hash).verified_contract %> +<% metadata_for_verification = SmartContract.get_address_verified_twin_contract(@address_hash).verified_contract %> <% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_vyper_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %>
<%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex index eaa5765bc29b..9e213f9457f0 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex @@ -27,46 +27,43 @@ ) %> - <%= case decoded_result do %> - <% {:error, :could_not_decode} -> %> -
<%= gettext "Decoded" %>
-
-
- <%= gettext "Failed to decode log data." %> -
- <% {:ok, method_id, text, mapping} -> %> -
<%= gettext "Decoded" %>
-
- - - - - - - - - -
Method Id0x<%= method_id %>
Call<%= text %>
- <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> - <% {:error, :contract_not_verified, results} -> %> - <%= for {:ok, method_id, text, mapping} <- results do %> -
<%= gettext "Decoded" %>
-
- - - - - - - - - -
Method Id0x<%= method_id %>
Call<%= text %>
- <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> -
+ <%= case decoded_result do %> + <% {:error, :could_not_decode} -> %> +
<%= gettext "Decoded" %>
+
+
+ <%= gettext "Failed to decode log data." %> +
+ <% {:ok, method_id, text, mapping} -> %> +
<%= gettext "Decoded" %>
+
+ + + + + + + + + +
Method Id0x<%= method_id %>
Call<%= text %>
+ <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> + <% {:error, :contract_not_verified, results} -> %> + <%= for {:ok, method_id, text, mapping} <- results do %> +
<%= gettext "Decoded" %>
+
+ + + + + + + + + +
Method Id0x<%= method_id %>
Call<%= text %>
+ <%= render BlockScoutWeb.LogView, "_data_decoded_view.html", mapping: mapping %> <% end %> - <% _ -> %> - <%= nil %> <% end %>
<%= gettext "Topics" %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex index 6f922375b973..3afb3d97a3f4 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token/overview.html.eex @@ -14,14 +14,14 @@ _ -> Decimal.new(0) end %> -<% data_usd_exchange_rate = +<% data_usd_exchange_rate = unless AddressView.empty_exchange_rate?(@exchange_rate) do "data-usd-exchange-rate='#{@exchange_rate.usd_value}' data-raw-usd-value='#{raw_usd_value}'" end %> <% native_coin_balance_token = AddressView.balance(@address) %> -<% native_coin_balance_usd = +<% native_coin_balance_usd = if AddressView.empty_exchange_rate?(@exchange_rate) do nil else @@ -32,7 +32,7 @@ " end %> -<% native_coin_balance = +<% native_coin_balance = if native_coin_balance_usd do native_coin_balance_usd <> " | " <> native_coin_balance_token else @@ -70,4 +70,4 @@ classes: ["fs-14"], container_classes: ["d-none"] %> -
\ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex index 20c47d29fe4b..550a85799498 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/advertisement/banners_ad/_banner_728.html.eex @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex index c8dc85c5621d..66eedb9a02f9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/advertisement/text_ad/index.html.eex @@ -1,4 +1,4 @@ \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex index d910c2ad1941..c498a395bebf 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex @@ -1,4 +1,4 @@ <%= link( gettext("Block #%{number}", number: to_string(@block.number)), - to: block_path(BlockScoutWeb.Endpoint, :show, @block) + to: block_path(BlockScoutWeb.Endpoint, :show, @block.hash) ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex index d7b9c2613400..419ac616db73 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex @@ -1,4 +1,4 @@ -<% burned_fee = if !is_nil(@block.base_fee_per_gas), do: Wei.mult(@block.base_fee_per_gas, BlockBurnedFeeCounter.fetch(@block.hash)), else: nil %> +<% burnt_fees = if !is_nil(@block.base_fee_per_gas), do: Wei.mult(@block.base_fee_per_gas, BlockBurntFeeCounter.fetch(@block.hash)), else: nil %> <% priority_fee = if !is_nil(@block.base_fee_per_gas), do: BlockPriorityFeeCounter.fetch(@block.hash), else: nil %>
@@ -33,7 +33,7 @@ <%= Cldr.Unit.new!(:byte, @block.size) |> cldr_unit_to_string!() %> <% end %> - +
<%= if !Application.get_env(:block_scout_web, :hide_block_miner) do %>
@@ -61,7 +61,7 @@ <%= format_wei_value(%Wei{value: priority_fee}, :ether) %> <%= gettext "Priority Fees" %> - <%= format_wei_value(burned_fee, :ether) %> <%= gettext "Burnt Fees" %> + <%= format_wei_value(burnt_fees, :ether) %> <%= gettext "Burnt Fees" %> <% end %> <%= formatted_gas(@block.gas_limit) %> <%= gettext "Gas Limit" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex index 332ae2b1392a..feee728643a1 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex @@ -1,4 +1,4 @@ -<% burned_fee = if !is_nil(@block.base_fee_per_gas), do: Wei.mult(@block.base_fee_per_gas, BlockBurnedFeeCounter.fetch(@block.hash)), else: nil %> +<% burnt_fees = if !is_nil(@block.base_fee_per_gas), do: Wei.mult(@block.base_fee_per_gas, BlockBurntFeeCounter.fetch(@block.hash)), else: nil %> <% priority_fee = if !is_nil(@block.base_fee_per_gas), do: BlockPriorityFeeCounter.fetch(@block.hash), else: nil %>
<%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> @@ -212,10 +212,10 @@
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", - text: Explorer.coin_name() <> " " <> gettext("burned from transactions included in the block (Base fee (per unit of gas) * Gas Used).") %> + text: Explorer.coin_name() <> " " <> gettext("burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used).") %> <%= gettext("Burnt Fees") %>
-
<%= format_wei_value(burned_fee, :ether) %>
+
<%= format_wei_value(burnt_fees, :ether) %>
@@ -226,7 +226,7 @@
<%= format_wei_value(%Wei{value: priority_fee}, :ether) %>
- <% end %> + <% end %> <%= if show_reward?(@block.rewards) do %>
<%= for block_reward <- @block.rewards do %> @@ -268,4 +268,4 @@
-<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> +<%= render BlockScoutWeb.Advertisement.BannersAdView, "_banner_728.html", conn: @conn %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex index 5f87d8debbf5..84fe3df3aa39 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/404.html.eex @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex index 4b84dce35873..f07899ea9bf8 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex @@ -8,7 +8,7 @@
-
<%= "#{gas_prices_from_oracle["average"]}" <> " " %><%= gettext "Gwei" %>
+
<%= "#{gas_prices_from_oracle[:average]}" <> " " %><%= gettext "Gwei" %>
-
<%= gettext "Slow" %><%= gas_prices_from_oracle["slow"] %> <%= gettext "Gwei" %>
-
<%= gettext "Average" %><%= gas_prices_from_oracle["average"] %> <%= gettext "Gwei" %>
-
<%= gettext "Fast" %><%= gas_prices_from_oracle["fast"] %> <%= gettext "Gwei" %>
+
<%= gettext "Slow" %><%= gas_prices_from_oracle[:slow] %> <%= gettext "Gwei" %>
+
<%= gettext "Average" %><%= gas_prices_from_oracle[:average] %> <%= gettext "Gwei" %>
+
<%= gettext "Fast" %><%= gas_prices_from_oracle[:fast] %> <%= gettext "Gwei" %>
" > @@ -40,4 +40,4 @@ <% end %> <% end %>
- \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index 33b1995cfa0f..9216229bc42b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -92,18 +92,20 @@
- <%= case @average_block_time do %> - <% {:error, :disabled} -> %> - <%= nil %> - <% average_block_time -> %> -
- - <%= gettext "Average block time" %> - - - <%= Timex.format_duration(average_block_time, Explorer.Counters.AverageBlockTimeDurationFormat) %> - -
+ <%= unless Application.get_env(:explorer, :chain_type) == "optimism" do %> + <%= case @average_block_time do %> + <% {:error, :disabled} -> %> + <%= nil %> + <% average_block_time -> %> +
+ + <%= gettext "Average block time" %> + + + <%= Timex.format_duration(average_block_time, Explorer.Counters.AverageBlockTimeDurationFormat) %> + +
+ <% end %> <% end %>
@@ -172,7 +174,7 @@ diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex index d36e97feccb7..135fe488e534 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_btn_copy.html.eex @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex index 979926300cf6..19e010c26e23 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/csv_export/index.html.eex @@ -10,7 +10,7 @@

<%= gettext "Export Data" %>

- <% filter_text = if Helper.is_valid_filter?(@filter_type, @filter_value, @type), do: " with applied filter by #{@filter_type} (#{@filter_value})", else: "" %> + <% filter_text = if Helper.valid_filter?(@filter_type, @filter_value, @type), do: " with applied filter by #{@filter_type} (#{@filter_value})", else: "" %>

<%= gettext("Export") %> <%= type_display_name(@type) %> <%= gettext("for address") %> <%= link( @address_hash_string, to: address_path(@conn, :show, @address_hash_string) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex index 54f032df05f8..962284ff61fe 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/form/_tag.html.eex @@ -1,3 +1,3 @@

"> <%= @text %> -
\ No newline at end of file +
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex index 27ede917dd8a..3c23ba78c6fa 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex @@ -33,7 +33,7 @@ to: block_path(BlockScoutWeb.Endpoint, :show, @internal_transaction.block_number) ) %> - + <%= if assigns[:current_address] do %> <%= if assigns[:current_address].hash == @internal_transaction.from_address_hash do %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 1f8707d57f69..7df4369402ac 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -83,7 +83,7 @@ <%= gettext("Tokens") %> -
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex index d1b33ffeeb91..fd1a1496f183 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex @@ -18,7 +18,7 @@ <%= BlockScoutWeb.TransactionView.value(@transaction, include_label: false) %> <%= Explorer.coin_name() %> - <%= BlockScoutWeb.TransactionView.formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %> + <%= BlockScoutWeb.TransactionView.formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex index c582d6747444..d7fa7e29c32b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -48,7 +48,7 @@ <%= value(@transaction, include_label: false) %> <%= Explorer.coin_name() %> - + <%= formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %> @@ -80,7 +80,7 @@ <%= @transaction |> block_number() |> BlockScoutWeb.RenderHelper.render_partial() %> - + <%= if from_or_to_address?(@transaction, assigns[:current_address]) do %> <%= if @transaction.from_address_hash == assigns[:current_address].hash do %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex index 4896e6e85312..9f3022941f31 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers.html.eex @@ -20,4 +20,4 @@ <% {:ok, value} -> %> <%= value %> <%= " " %><%= render BlockScoutWeb.TransactionView, "_link_to_token_symbol.html", transfer: @transfer %> -<% end %> \ No newline at end of file +<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex index d26557b9c32a..91de91125ad1 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex @@ -40,7 +40,7 @@ style: "position: relative;"%> For -<% end %> +<% end %> <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: @transfer %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex index 08a92a4fb370..72fa05e3b930 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -7,7 +7,7 @@ <% base_fee_per_gas = if block, do: block.base_fee_per_gas, else: nil %> <% max_priority_fee_per_gas = @transaction.max_priority_fee_per_gas %> <% max_fee_per_gas = @transaction.max_fee_per_gas %> -<% burned_fee = +<% burnt_fees = if !is_nil(max_fee_per_gas) and !is_nil(@transaction.gas_used) and !is_nil(base_fee_per_gas) do if Decimal.compare(max_fee_per_gas.value, 0) == :eq do %Wei{value: Decimal.new(0)} @@ -17,7 +17,7 @@ else nil end %> -<% %Wei{value: burned_fee_decimal} = if is_nil(burned_fee), do: %Wei{value: Decimal.new(0)}, else: burned_fee %> +<% %Wei{value: burnt_fee_decimal} = if is_nil(burnt_fees), do: %Wei{value: Decimal.new(0)}, else: burnt_fees %> <% priority_fee_per_gas = if is_nil(max_priority_fee_per_gas) or is_nil(base_fee_per_gas), do: nil, else: Enum.min_by([max_priority_fee_per_gas, Wei.sub(max_fee_per_gas, base_fee_per_gas)], fn x -> Wei.to(x, :wei) end) %> <% priority_fee = if is_nil(priority_fee_per_gas), do: nil, else: Wei.mult(priority_fee_per_gas, @transaction.gas_used) %> <% decoded_input_data = decoded_input_data(@transaction) %> @@ -122,7 +122,7 @@ <% true -> %> <%= render BlockScoutWeb.FormView, "_tag.html", text: formatted_status, additional_classes: ["success", "large"] %> <% end %> - + <%= if confirmations > 0 do %> <%= gettext "Confirmed by " %><%= confirmations %><%= " " <> confirmations_ds_name(confirmations) %> <% end %> @@ -195,6 +195,26 @@ <% end %> + <%= if Application.get_env(:explorer, :chain_type) == "optimism" && @transaction.l1_block_number do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Block number containing the transaction on L1.") %> + <%= gettext "L1 Block" %>
+
+ <%= if block do %> + <%= link( + @transaction.l1_block_number, + class: "transaction__link", + to: "https://eth-goerli.blockscout.com/block/#{@transaction.l1_block_number}" + ) %> + <% else %> + <%= formatted_result(status) %> + <% end %> +
+
+ <% end %> <% %{transaction_actions: transaction_actions} = transaction_actions(@transaction) %> <%= if not Enum.empty?(transaction_actions) do %> @@ -371,12 +391,18 @@ <% end %> - +
- <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", - text: gettext("Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage.") %> - <%= gettext "Gas Price" %> + <%= if Application.get_env(:explorer, :chain_type) == "optimism" do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage.") %> + <%= gettext "L2 Gas Price" %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage.") %> + <%= gettext "Gas Price" %> + <% end %>
<%= gas_price(@transaction, :gwei) %>
@@ -394,9 +420,15 @@
- <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", - text: gettext("Maximum gas amount approved for the transaction.") %> - <%= gettext "Gas Limit" %> + <%= if Application.get_env(:explorer, :chain_type) == "optimism" do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Maximum gas amount approved for the transaction on L2.") %> + <%= gettext "L2 Gas Limit" %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Maximum gas amount approved for the transaction.") %> + <%= gettext "Gas Limit" %> + <% end %>
<%= format_gas_limit(@transaction.gas) %>
@@ -429,31 +461,72 @@
<%= format_wei_value(priority_fee, :ether) %>
- <% end %> - <%= if !is_nil(burned_fee) do %> + <% end %> + <%= if !is_nil(burnt_fees) do %>
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", - text: gettext("Amount of") <> " " <> Explorer.coin_name() <> " " <> gettext("burned for this transaction. Equals Block Base Fee per Gas * Gas Used.") %> + text: gettext("Amount of") <> " " <> Explorer.coin_name() <> " " <> gettext("burnt for this transaction. Equals Block Base Fee per Gas * Gas Used.") %> <%= gettext "Transaction Burnt Fee" %>
-
<%= format_wei_value(burned_fee, :ether) %> +
<%= format_wei_value(burnt_fees, :ether) %> <%= unless empty_exchange_rate?(@exchange_rate) do %> - ( data-usd-exchange-rate=<%= @exchange_rate.usd_value %>>) + ( data-usd-exchange-rate=<%= @exchange_rate.usd_value %>>) <% end %>
<% end %> - +
- <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", - text: gettext("Actual gas amount used by the transaction.") %> - <%= gettext "Gas Used by Transaction" %> + <%= if Application.get_env(:explorer, :chain_type) == "optimism" do %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Actual gas amount used by the transaction on L2.") %> + <%= gettext "L2 Gas Used by Transaction" %> + <% else %> + <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Actual gas amount used by the transaction.") %> + <%= gettext "Gas Used by Transaction" %> + <% end %>
<% gas_used_perc = gas_used_perc(@transaction) %>
<%= gas_used(@transaction) %> <%= if gas_used_perc, do: "| #{gas_used_perc}%" %>
+ <%= if Application.get_env(:explorer, :chain_type) == "optimism" do %> + <%= if @transaction.l1_gas_used do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("L1 Gas Used by Transaction") %> + <%= gettext "L1 Gas Used by Transaction" %> +
+
<%= l1_gas_used(@transaction) %>
+
+ <% end %> + <%= if @transaction.l1_gas_used do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("L1 Gas Price") %> + <%= gettext "L1 Gas Price" %> +
+
<%= l1_gas_price(@transaction, :gwei) %>
+
+ <% end %> + <%= if @transaction.l1_fee_scalar do %> + +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("L1 Fee Scalar") %> + <%= gettext "L1 Fee Scalar" %> +
+
<%= @transaction.l1_fee_scalar %>
+
+ <% end %> + <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex new file mode 100644 index 000000000000..572133156dd5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/utils_api_v2_router.ex @@ -0,0 +1,24 @@ +# This file in ignore list of `sobelow`, be careful while adding new endpoints here +defmodule BlockScoutWeb.UtilsApiV2Router do + @moduledoc """ + Router for /api/v2/utils. This route has separate router in order to ignore sobelow's warning about missing CSRF protection + """ + use BlockScoutWeb, :router + alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + + pipeline :api_v2_no_forgery_protect do + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) + plug(:accepts, ["json"]) + plug(CheckApiV2) + plug(RateLimit) + plug(:fetch_session) + end + + scope "/", as: :api_v2 do + pipe_through(:api_v2_no_forgery_protect) + alias BlockScoutWeb.API.V2 + + get("/decode-calldata", V2.UtilsController, :decode_calldata) + post("/decode-calldata", V2.UtilsController, :decode_calldata) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex index d0fb8d0c574a..ecfb0ffe556e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex @@ -93,10 +93,10 @@ defmodule BlockScoutWeb.AccessHelper do Enum.member?(whitelisted_ips(rate_limit_config), ip_string) -> rate_limit(ip_string, limit_by_whitelisted_ip, time_interval_limit) - is_api_v2_request?(conn) && !is_nil(token) && !is_nil(user_agent) -> + api_v2_request?(conn) && !is_nil(token) && !is_nil(user_agent) -> rate_limit(token, api_v2_ui_limit, time_interval_limit) - is_api_v2_request?(conn) && !is_nil(user_agent) -> + api_v2_request?(conn) && !is_nil(user_agent) -> rate_limit(ip_string, limit_by_ip, time_interval_by_ip) true -> @@ -155,8 +155,8 @@ defmodule BlockScoutWeb.AccessHelper do end end - defp is_api_v2_request?(%Plug.Conn{request_path: "/api/v2/" <> _}), do: true - defp is_api_v2_request?(_), do: false + defp api_v2_request?(%Plug.Conn{request_path: "/api/v2/" <> _}), do: true + defp api_v2_request?(_), do: false def conn_to_ip_string(conn) do is_blockscout_behind_proxy = Application.get_env(:block_scout_web, :api_rate_limit)[:is_blockscout_behind_proxy] @@ -171,7 +171,7 @@ defmodule BlockScoutWeb.AccessHelper do api_v2_temp_token_key = Application.get_env(:block_scout_web, :api_v2_temp_token_key) conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_key]) - case is_api_v2_request?(conn) && conn.cookies[api_v2_temp_token_key] do + case api_v2_request?(conn) && conn.cookies[api_v2_temp_token_key] do %{ip: ^ip_string} -> conn.req_cookies[api_v2_temp_token_key] diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex index 26c8d7c8295b..553d01206ad9 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex @@ -112,12 +112,15 @@ defmodule BlockScoutWeb.Account.Api.V1.UserView do "ERC-721" => %{ "incoming" => watchlist.watch_erc_721_input, "outcoming" => watchlist.watch_erc_721_output - } - # , + }, # "ERC-1155" => %{ # "incoming" => watchlist.watch_erc_1155_input, # "outcoming" => watchlist.watch_erc_1155_output - # } + # }, + "ERC-404" => %{ + "incoming" => watchlist.watch_erc_404_input, + "outcoming" => watchlist.watch_erc_404_output + } }, "notification_methods" => %{ "email" => watchlist.notify_email diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex index f6e676d7fcdf..e5adf9b9d4ae 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex @@ -1,7 +1,6 @@ defmodule BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView do use BlockScoutWeb, :view - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.RustVerifierInterface end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex index 12a80e5eb282..76f88f059ac0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex @@ -1,7 +1,6 @@ defmodule BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView do use BlockScoutWeb, :view - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.RustVerifierInterface end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex index cf45efe96f0a..9a4298a01c4b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_standard_json_input_view.ex @@ -1,6 +1,5 @@ defmodule BlockScoutWeb.AddressContractVerificationViaStandardJsonInputView do use BlockScoutWeb, :view - alias Explorer.Chain alias Explorer.Chain.SmartContract end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex index e0ebba9694a1..47da5eab9093 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_vyper_view.ex @@ -1,6 +1,5 @@ defmodule BlockScoutWeb.AddressContractVerificationVyperView do use BlockScoutWeb, :view - alias Explorer.Chain alias Explorer.Chain.SmartContract end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex index 608971ec7af6..01b13e6a71d3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex @@ -1,11 +1,15 @@ defmodule BlockScoutWeb.AddressContractView do use BlockScoutWeb, :view + require Logger + import Explorer.Helper, only: [decode_data: 2] alias ABI.FunctionSelector alias Explorer.Chain alias Explorer.Chain.{Address, Data, InternalTransaction, Transaction} + alias Explorer.Chain.SmartContract + alias Explorer.Chain.SmartContract.Proxy.EIP1167 def render("scripts.html", %{conn: conn}) do render_scripts(conn, "address_contract/code_highlighting.js") @@ -130,6 +134,12 @@ defmodule BlockScoutWeb.AddressContractView do chain_id = Application.get_env(:explorer, Explorer.ThirdPartyIntegrations.Sourcify)[:chain_id] repo_url = Application.get_env(:explorer, Explorer.ThirdPartyIntegrations.Sourcify)[:repo_url] match = if partial_match, do: "/partial_match/", else: "/full_match/" - repo_url <> match <> chain_id <> "/" <> checksummed_hash <> "/" + + if chain_id do + repo_url <> match <> chain_id <> "/" <> checksummed_hash <> "/" + else + Logger.warning("chain_id is nil. Please set CHAIN_ID env variable.") + nil + end end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 41ebf80976d6..532aceed6b5f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.AddressView do alias Explorer.Chain.Address.Counters alias Explorer.Chain.{Address, Hash, InternalTransaction, Log, SmartContract, Token, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Block.Reward + alias Explorer.Chain.SmartContract.Proxy alias Explorer.ExchangeRates.Token, as: TokenExchangeRate alias Explorer.SmartContract.{Helper, Writer} @@ -199,7 +200,7 @@ defmodule BlockScoutWeb.AddressView do def primary_name(%Address{names: _} = address) do with false <- is_nil(address.contract_code), - twin <- Chain.get_verified_twin_contract(address), + twin <- SmartContract.get_verified_twin_contract(address), false <- is_nil(twin) do twin.name else @@ -255,20 +256,20 @@ defmodule BlockScoutWeb.AddressView do def smart_contract_verified?(%Address{smart_contract: nil}), do: false def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do - Enum.any?(address.smart_contract.abi || [], &is_read_function?(&1)) + Enum.any?(address.smart_contract.abi || [], &read_function?(&1)) end - def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false + def smart_contract_with_read_only_functions?(%Address{smart_contract: _}), do: false - def is_read_function?(function), do: Helper.queriable_method?(function) || Helper.read_with_wallet_method?(function) + def read_function?(function), do: Helper.queriable_method?(function) || Helper.read_with_wallet_method?(function) def smart_contract_is_proxy?(address, options \\ []) def smart_contract_is_proxy?(%Address{smart_contract: %SmartContract{} = smart_contract}, options) do - SmartContract.proxy_contract?(smart_contract, options) + Proxy.proxy_contract?(smart_contract, options) end - def smart_contract_is_proxy?(%Address{smart_contract: nil}, _), do: false + def smart_contract_is_proxy?(%Address{smart_contract: _}, _), do: false def smart_contract_with_write_functions?(%Address{smart_contract: %SmartContract{}} = address) do !contract_interaction_disabled?() && @@ -278,7 +279,7 @@ defmodule BlockScoutWeb.AddressView do ) end - def smart_contract_with_write_functions?(%Address{smart_contract: nil}), do: false + def smart_contract_with_write_functions?(%Address{smart_contract: _}), do: false def has_decompiled_code?(address) do address.has_decompiled_code? || @@ -456,7 +457,7 @@ defmodule BlockScoutWeb.AddressView do end def smart_contract_is_gnosis_safe_proxy?(%Address{smart_contract: %SmartContract{}} = address) do - address.smart_contract.name == "GnosisSafeProxy" && Chain.gnosis_safe_contract?(address.smart_contract.abi) + address.smart_contract.name == "GnosisSafeProxy" && Proxy.gnosis_safe_contract?(address.smart_contract.abi) end def smart_contract_is_gnosis_safe_proxy?(_address), do: false @@ -479,7 +480,7 @@ defmodule BlockScoutWeb.AddressView do end def check_custom_abi_for_having_read_functions(custom_abi), - do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &is_read_function?(&1)) + do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &read_function?(&1)) def has_address_custom_abi_with_write_functions?(conn, address_hash) do if contract_interaction_disabled?() do @@ -496,6 +497,14 @@ defmodule BlockScoutWeb.AddressView do def contract_interaction_disabled?, do: Application.get_env(:block_scout_web, :contract)[:disable_interaction] + @doc """ + Decodes given log + """ + @spec decode(Log.t(), Transaction.t()) :: + {:ok, String.t(), String.t(), map()} + | {:error, atom()} + | {:error, atom(), list()} + | {{:error, :contract_not_verified, list()}, any()} def decode(log, transaction) do {result, _contracts_acc, _events_acc} = Log.decode(log, transaction, [], true) result diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex index 16f85b303ff2..d2b29c0117c6 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/eth_rpc/view.ex @@ -59,6 +59,14 @@ defmodule BlockScoutWeb.API.EthRPC.View do """ end + def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) when is_map(error) do + error = Poison.encode!(error) + + """ + {"jsonrpc":"2.0","error": #{error},"id": #{id}} + """ + end + def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) do """ {"jsonrpc":"2.0","error": "#{error}","id": #{id}} @@ -75,6 +83,14 @@ defmodule BlockScoutWeb.API.EthRPC.View do """ end + def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) when is_map(error) do + error = Jason.encode!(error) + + """ + {"jsonrpc":"2.0","error": #{error},"id": #{id}} + """ + end + def encode(%BlockScoutWeb.API.EthRPC.View{id: id, error: error}, _options) do """ {"jsonrpc":"2.0","error": "#{error}","id": #{id}} diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 2d9deed6efb4..eca2f51a14e0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -43,6 +43,11 @@ defmodule BlockScoutWeb.API.RPC.AddressView do RPCView.render("show.json", data: data) end + def render("tokennfttx.json", %{token_transfers: token_transfers, max_block_number: max_block_number}) do + data = Enum.map(token_transfers, &prepare_nft_transfer(&1, max_block_number)) + RPCView.render("show.json", data: data) + end + def render("tokenbalance.json", %{token_balance: token_balance}) do RPCView.render("show.json", data: to_string(token_balance)) end @@ -113,7 +118,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do "to" => "#{transaction.to_address_hash}", "value" => "#{transaction.value.value}", "gas" => "#{transaction.gas}", - "gasPrice" => "#{transaction.gas_price.value}", + "gasPrice" => "#{transaction.gas_price && transaction.gas_price.value}", "isError" => if(transaction.status == :ok, do: "0", else: "1"), "txreceipt_status" => if(transaction.status == :ok, do: "1", else: "0"), "input" => "#{transaction.input}", @@ -160,7 +165,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do "tokenDecimal" => to_string(token_transfer.token_decimals), "transactionIndex" => to_string(token_transfer.transaction_index), "gas" => to_string(token_transfer.transaction_gas), - "gasPrice" => to_string(token_transfer.transaction_gas_price.value), + "gasPrice" => to_string(token_transfer.transaction_gas_price && token_transfer.transaction_gas_price.value), "gasUsed" => to_string(token_transfer.transaction_gas_used), "cumulativeGasUsed" => to_string(token_transfer.transaction_cumulative_gas_used), "input" => to_string(token_transfer.transaction_input), @@ -187,6 +192,13 @@ defmodule BlockScoutWeb.API.RPC.AddressView do |> Map.put_new(:values, token_transfer.amounts) end + defp prepare_token_transfer(%{token_type: "ERC-404"} = token_transfer) do + token_transfer + |> prepare_common_token_transfer() + |> Map.put_new(:tokenIDs, token_transfer.token_ids) + |> Map.put_new(:values, token_transfer.amounts) + end + defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do token_transfer |> prepare_common_token_transfer() @@ -197,6 +209,31 @@ defmodule BlockScoutWeb.API.RPC.AddressView do prepare_common_token_transfer(token_transfer) end + defp prepare_nft_transfer(token_transfer, max_block_number) do + %{ + "blockNumber" => to_string(token_transfer.block_number), + "timeStamp" => to_string(DateTime.to_unix(token_transfer.block.timestamp)), + "hash" => to_string(token_transfer.transaction_hash), + "nonce" => to_string(token_transfer.transaction.nonce), + "blockHash" => to_string(token_transfer.block_hash), + "from" => to_string(token_transfer.from_address_hash), + "contractAddress" => to_string(token_transfer.token_contract_address_hash), + "to" => to_string(token_transfer.to_address_hash), + "tokenID" => to_string(List.first(token_transfer.token_ids)), + "logIndex" => to_string(token_transfer.log_index), + "tokenName" => token_transfer.token.name, + "tokenSymbol" => token_transfer.token.symbol, + "tokenDecimal" => to_string(token_transfer.token.decimals || 0), + "transactionIndex" => to_string(token_transfer.transaction.index), + "gas" => to_string(token_transfer.transaction.gas), + "gasPrice" => to_string(token_transfer.transaction.gas_price && token_transfer.transaction.gas_price.value), + "gasUsed" => to_string(token_transfer.transaction.gas_used), + "cumulativeGasUsed" => to_string(token_transfer.transaction.cumulative_gas_used), + "input" => "deprecated", + "confirmations" => to_string(max_block_number - token_transfer.block_number) + } + end + defp prepare_block(block) do %{ "blockNumber" => to_string(block.number), diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex index 70d5f0bd0441..b0262aaf937c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex @@ -3,22 +3,75 @@ defmodule BlockScoutWeb.API.RPC.BlockView do alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView alias BlockScoutWeb.API.RPC.RPCView - alias Explorer.Chain.{Hash, Wei} + alias Explorer.Chain.{Block, Hash, Wei} alias Explorer.EthRPC, as: EthRPC - def render("block_reward.json", %{block: block, reward: reward}) do + def render("block_reward.json", %{block: %Block{rewards: [_ | _]} = block}) do reward_as_string = - reward + block.rewards + |> Enum.find(%{reward: %Wei{value: Decimal.new(0)}}, &(&1.address_type == :validator)) + |> Map.get(:reward) |> Wei.to(:wei) |> Decimal.to_string(:normal) + static_reward = + block.rewards + |> Enum.find(%{reward: %Wei{value: Decimal.new(0)}}, &(&1.address_type == :emission_funds)) + |> Map.get(:reward) + |> Wei.to(:wei) + + uncles = + block.rewards + |> Stream.filter(&(&1.address_type == :uncle)) + |> Stream.with_index() + |> Enum.map(fn {reward, index} -> + %{ + "unclePosition" => to_string(index), + "miner" => Hash.to_string(reward.address_hash), + "blockreward" => reward.reward |> Wei.to(:wei) |> Decimal.to_string(:normal) + } + end) + data = %{ "blockNumber" => to_string(block.number), "timeStamp" => DateTime.to_unix(block.timestamp), "blockMiner" => Hash.to_string(block.miner_hash), "blockReward" => reward_as_string, - "uncles" => nil, - "uncleInclusionReward" => nil + "uncles" => uncles, + "uncleInclusionReward" => + static_reward + |> Decimal.mult(Enum.count(uncles)) + |> Decimal.div(Block.uncle_reward_coef()) + |> Decimal.to_string(:normal) + } + + RPCView.render("show.json", data: data) + end + + def render("block_reward.json", %{block: block}) do + data = %{ + "blockNumber" => to_string(block.number), + "timeStamp" => DateTime.to_unix(block.timestamp), + "blockMiner" => Hash.to_string(block.miner_hash), + "blockReward" => "0", + "uncles" => [], + "uncleInclusionReward" => "0" + } + + RPCView.render("show.json", data: data) + end + + def render("block_countdown.json", %{ + current_block: current_block, + countdown_block: countdown_block, + remaining_blocks: remaining_blocks, + estimated_time_in_sec: estimated_time_in_sec + }) do + data = %{ + "CurrentBlock" => to_string(current_block), + "CountdownBlock" => to_string(countdown_block), + "RemainingBlock" => to_string(remaining_blocks), + "EstimateTimeInSec" => to_string(estimated_time_in_sec) } RPCView.render("show.json", data: data) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex index d1686cef1565..24178233be63 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex @@ -4,11 +4,16 @@ defmodule BlockScoutWeb.API.RPC.ContractView do alias BlockScoutWeb.AddressView alias BlockScoutWeb.API.RPC.RPCView alias Ecto.Association.NotLoaded - alias Explorer.Chain alias Explorer.Chain.{Address, DecompiledSmartContract, SmartContract} defguardp is_empty_string(input) when input == "" or input == nil + def render("getcontractcreation.json", %{addresses: addresses}) do + contracts = addresses |> Enum.map(&address_to_response/1) |> Enum.reject(&is_nil/1) + + RPCView.render("show.json", data: contracts) + end + def render("listcontracts.json", %{contracts: contracts}) do contracts = Enum.map(contracts, &prepare_contract/1) @@ -168,11 +173,11 @@ defmodule BlockScoutWeb.API.RPC.ContractView do end defp insert_additional_sources(output, address) do - additional_sources_from_twin = Chain.get_address_verified_twin_contract(address.hash).additional_sources + additional_sources_from_twin = SmartContract.get_address_verified_twin_contract(address.hash).additional_sources additional_sources = if AddressView.smart_contract_verified?(address), - do: address.smart_contract_additional_sources, + do: address.smart_contract.smart_contract_additional_sources, else: additional_sources_from_twin additional_sources_array = @@ -230,4 +235,16 @@ defmodule BlockScoutWeb.API.RPC.ContractView do defp decompiler_version(nil), do: "" defp decompiler_version(%DecompiledSmartContract{decompiler_version: decompiler_version}), do: decompiler_version + + defp address_to_response(address) do + creator_hash = AddressView.from_address_hash(address) + creation_tx = creator_hash && AddressView.transaction_hash(address) + + creation_tx && + %{ + "contractAddress" => to_string(address.hash), + "contractCreator" => to_string(creator_hash), + "txHash" => to_string(creation_tx) + } + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex index 6f2933d7c63b..f52ab8fadccd 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/logs_view.ex @@ -44,6 +44,8 @@ defmodule BlockScoutWeb.API.RPC.LogsView do |> integer_to_hex() end + defp datetime_to_hex(nil), do: nil + defp datetime_to_hex(datetime) do datetime |> DateTime.to_unix() diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex index 91f6967070da..16635461e057 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex @@ -19,8 +19,12 @@ defmodule BlockScoutWeb.API.RPC.StatsView do RPCView.render("show_value.json", data: total_supply) end + def render("ethprice.json", %{rates: rates}) do + RPCView.render("show.json", data: prepare_rates(rates, "eth")) + end + def render("coinprice.json", %{rates: rates}) do - RPCView.render("show.json", data: prepare_rates(rates)) + RPCView.render("show.json", data: prepare_rates(rates, "coin_")) end def render("totalfees.json", %{total_fees: total_fees}) do @@ -31,22 +35,22 @@ defmodule BlockScoutWeb.API.RPC.StatsView do RPCView.render("error.json", assigns) end - defp prepare_rates(rates) do + defp prepare_rates(rates, prefix) do if rates do - timestamp = rates.last_updated |> DateTime.to_unix() |> to_string() + timestamp = rates.last_updated && rates.last_updated |> DateTime.to_unix() |> to_string() %{ - "coin_btc" => to_string(rates.btc_value), - "coin_btc_timestamp" => timestamp, - "coin_usd" => to_string(rates.usd_value), - "coin_usd_timestamp" => timestamp + (prefix <> "btc") => rates.btc_value && to_string(rates.btc_value), + (prefix <> "btc_timestamp") => timestamp, + (prefix <> "usd") => rates.usd_value && to_string(rates.usd_value), + (prefix <> "usd_timestamp") => timestamp } else %{ - "coin_btc" => nil, - "coin_btc_timestamp" => nil, - "coin_usd" => nil, - "coin_usd_timestamp" => nil + (prefix <> "btc") => nil, + (prefix <> "btc_timestamp") => nil, + (prefix <> "usd") => nil, + (prefix <> "usd_timestamp") => nil } end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex index 9ccab7c9d163..693e5c3c72ab 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.RPC.TokenView do use BlockScoutWeb, :view alias BlockScoutWeb.API.RPC.RPCView + alias BlockScoutWeb.{BridgedTokensView, CurrencyHelper} def render("gettoken.json", %{token: token}) do RPCView.render("show.json", data: prepare_token(token)) @@ -12,6 +13,11 @@ defmodule BlockScoutWeb.API.RPC.TokenView do RPCView.render("show.json", data: data) end + def render("bridgedtokenlist.json", %{bridged_tokens: bridged_tokens}) do + data = Enum.map(bridged_tokens, &prepare_bridged_token/1) + RPCView.render("show.json", data: data) + end + def render("error.json", assigns) do RPCView.render("error.json", assigns) end @@ -34,4 +40,21 @@ defmodule BlockScoutWeb.API.RPC.TokenView do "value" => token_holder.value } end + + defp prepare_bridged_token({token, bridged_token}) do + total_supply = CurrencyHelper.divide_decimals(token.total_supply, token.decimals) + usd_value = BridgedTokensView.bridged_token_usd_cap(bridged_token, token) + + %{ + "foreignChainId" => bridged_token.foreign_chain_id, + "foreignTokenContractAddressHash" => bridged_token.foreign_token_contract_address_hash, + "homeContractAddressHash" => token.contract_address_hash, + "homeDecimals" => token.decimals, + "homeHolderCount" => if(token.holder_count, do: to_string(token.holder_count), else: "0"), + "homeName" => token.name, + "homeSymbol" => token.symbol, + "homeTotalSupply" => total_supply, + "homeUsdValue" => usd_value + } + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index 4b81b110914f..4a18643aa354 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do use BlockScoutWeb, :view alias BlockScoutWeb.API.RPC.RPCView + alias Explorer.Chain.Transaction def render("gettxinfo.json", %{ transaction: transaction, @@ -58,7 +59,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do defp prepare_transaction(transaction, block_height, logs, next_page_params) do %{ "hash" => "#{transaction.hash}", - "timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}", + "timeStamp" => "#{DateTime.to_unix(Transaction.block_timestamp(transaction))}", "blockNumber" => "#{transaction.block_number}", "confirmations" => "#{block_height - transaction.block_number}", "success" => if(transaction.status == :ok, do: true, else: false), diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 9cdf88abb2f9..0c177f3c1957 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do alias Explorer.{Chain, Market} alias Explorer.Chain.Address.Counters alias Explorer.Chain.{Address, SmartContract} + alias Explorer.Chain.Token.Instance @api_true [api?: true] @@ -54,20 +55,45 @@ defmodule BlockScoutWeb.API.V2.AddressView do } end - def prepare_address({address, nonce}) do + def render("nft_list.json", %{token_instances: token_instances, token: token, next_page_params: next_page_params}) do + %{"items" => Enum.map(token_instances, &prepare_nft(&1, token)), "next_page_params" => next_page_params} + end + + def render("nft_list.json", %{token_instances: token_instances, next_page_params: next_page_params}) do + %{"items" => Enum.map(token_instances, &prepare_nft(&1)), "next_page_params" => next_page_params} + end + + def render("nft_collections.json", %{collections: nft_collections, next_page_params: next_page_params}) do + %{"items" => Enum.map(nft_collections, &prepare_nft_collection(&1)), "next_page_params" => next_page_params} + end + + @spec prepare_address( + {atom() | %{:fetched_coin_balance => any(), :hash => any(), optional(any()) => any()}, any()} + | Explorer.Chain.Address.t() + ) :: %{optional(:coin_balance) => any(), optional(:tx_count) => binary(), optional(<<_::32, _::_*8>>) => any()} + def prepare_address({address, tx_count}) do nil |> Helper.address_with_info(address, address.hash, true) - |> Map.put(:tx_count, to_string(nonce)) + |> Map.put(:tx_count, to_string(tx_count)) |> Map.put(:coin_balance, if(address.fetched_coin_balance, do: address.fetched_coin_balance.value)) end def prepare_address(address, conn \\ nil) do base_info = Helper.address_with_info(conn, address, address.hash, true) - is_proxy = AddressView.smart_contract_is_proxy?(address, @api_true) + + {:ok, address_with_smart_contract} = + Chain.hash_to_address( + address.hash, + [necessity_by_association: %{:smart_contract => :optional}], + false + ) + + is_proxy = AddressView.smart_contract_is_proxy?(address_with_smart_contract, @api_true) {implementation_address, implementation_name} = with true <- is_proxy, - {address, name} <- SmartContract.get_implementation_address_hash(address.smart_contract, @api_true), + {address, name} <- + SmartContract.get_implementation_address_hash(address_with_smart_contract.smart_contract, @api_true), false <- is_nil(address), {:ok, address_hash} <- Chain.string_to_address_hash(address), checksummed_address <- Address.checksum(address_hash) do @@ -101,7 +127,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do "has_methods_read" => AddressView.smart_contract_with_read_only_functions?(address), "has_methods_write" => AddressView.smart_contract_with_write_functions?(address), "has_methods_read_proxy" => is_proxy, - "has_methods_write_proxy" => AddressView.smart_contract_with_write_functions?(address) && is_proxy, + "has_methods_write_proxy" => + AddressView.smart_contract_with_write_functions?(address_with_smart_contract) && is_proxy, "has_decompiled_code" => AddressView.has_decompiled_code?(address), "has_validated_blocks" => Counters.check_if_validated_blocks_at_address(address.hash, @api_true), "has_logs" => Counters.check_if_logs_at_address(address.hash, @api_true), @@ -112,7 +139,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do }) end - def prepare_token_balance(token_balance, fetch_token_instance? \\ false) do + @spec prepare_token_balance(Chain.Address.TokenBalance.t(), boolean()) :: map() + defp prepare_token_balance(token_balance, fetch_token_instance? \\ false) do %{ "value" => token_balance.value, "token" => TokenView.render("token.json", %{token: token_balance.token}), @@ -123,7 +151,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do fetch_and_render_token_instance( token_balance.token_id, token_balance.token, - token_balance.address_hash + token_balance.address_hash, + token_balance ) ) } @@ -156,18 +185,74 @@ defmodule BlockScoutWeb.API.V2.AddressView do end end - def fetch_and_render_token_instance(token_id, token, address_hash) do + defp prepare_nft(nft) do + prepare_nft(nft, nft.token) + end + + defp prepare_nft(nft, token) do + Map.merge( + %{"token_type" => token.type, "value" => value(token.type, nft)}, + TokenView.prepare_token_instance(nft, token) + ) + end + + defp prepare_nft_collection(collection) do + %{ + "token" => TokenView.render("token.json", token: collection.token), + "amount" => string_or_null(collection.distinct_token_instances_count || collection.value), + "token_instances" => + Enum.map(collection.preloaded_token_instances, fn instance -> + prepare_nft_for_collection(collection.token.type, instance) + end) + } + end + + defp prepare_nft_for_collection(token_type, instance) do + Map.merge( + %{"token_type" => token_type, "value" => value(token_type, instance)}, + TokenView.prepare_token_instance(instance, nil) + ) + end + + defp value("ERC-721", _), do: "1" + defp value(_, nft), do: nft.current_token_balance && to_string(nft.current_token_balance.value) + + defp string_or_null(nil), do: nil + defp string_or_null(other), do: to_string(other) + + # TODO think about this approach mb refactor or mark deprecated for example. + # Suggested solution: batch preload + @spec fetch_and_render_token_instance( + Decimal.t(), + Ecto.Schema.belongs_to(Chain.Token.t()) | nil, + Chain.Hash.Address.t(), + Chain.Address.TokenBalance.t() + ) :: map() + def fetch_and_render_token_instance(token_id, token, address_hash, token_balance) do token_instance = - case Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address( + case Chain.nft_instance_from_token_id_and_token_address( token_id, token.contract_address_hash, @api_true ) do # `%{hash: address_hash}` will match with `address_with_info(_, address_hash)` clause in `BlockScoutWeb.API.V2.Helper` - {:ok, token_instance} -> %{token_instance | owner: %{hash: address_hash}} - {:error, :not_found} -> %{token_id: token_id, metadata: nil, owner: %{hash: address_hash}} + {:ok, token_instance} -> + %Instance{token_instance | owner: %{hash: address_hash}, current_token_balance: token_balance} + + {:error, :not_found} -> + %Instance{ + token_id: token_id, + metadata: nil, + owner: %Address{hash: address_hash}, + current_token_balance: token_balance, + token_contract_address_hash: token.contract_address_hash + } + |> Instance.put_is_unique(token, @api_true) end - TokenView.render("token_instance.json", %{token_instance: token_instance, token: token}) + TokenView.render("token_instance.json", %{ + token_instance: token_instance, + token: token + }) end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/blob_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/blob_view.ex new file mode 100644 index 000000000000..fcb792ac095e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/blob_view.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.API.V2.BlobView do + use BlockScoutWeb, :view + + alias Explorer.Chain.Beacon.Blob + + def render("blob.json", %{blob: blob, transaction_hashes: transaction_hashes}) do + blob |> prepare_blob() |> Map.put("transaction_hashes", transaction_hashes) + end + + def render("blobs.json", %{blobs: blobs}) do + %{"items" => Enum.map(blobs, &prepare_blob(&1))} + end + + @spec prepare_blob(Blob.t()) :: map() + def prepare_blob(blob) do + %{ + "hash" => blob.hash, + "blob_data" => blob.blob_data, + "kzg_commitment" => blob.kzg_commitment, + "kzg_proof" => blob.kzg_proof + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index c4793dbacedf..ba23d27f3d60 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -3,7 +3,6 @@ defmodule BlockScoutWeb.API.V2.BlockView do alias BlockScoutWeb.BlockView alias BlockScoutWeb.API.V2.{ApiView, Helper} - alias Explorer.Chain alias Explorer.Chain.Block alias Explorer.Counters.BlockPriorityFeeCounter @@ -29,10 +28,10 @@ defmodule BlockScoutWeb.API.V2.BlockView do end def prepare_block(block, _conn, single_block? \\ false) do - burned_fee = Chain.burned_fees(block.transactions, block.base_fee_per_gas) + burnt_fees = Block.burnt_fees(block.transactions, block.base_fee_per_gas) priority_fee = block.base_fee_per_gas && BlockPriorityFeeCounter.fetch(block.hash) - tx_fees = Chain.txn_fees(block.transactions) + transaction_fees = Block.transaction_fees(block.transactions) %{ "height" => block.number, @@ -48,19 +47,20 @@ defmodule BlockScoutWeb.API.V2.BlockView do "gas_limit" => block.gas_limit, "nonce" => block.nonce, "base_fee_per_gas" => block.base_fee_per_gas, - "burnt_fees" => burned_fee, + "burnt_fees" => burnt_fees, "priority_fee" => priority_fee, # "extra_data" => "TODO", "uncles_hashes" => prepare_uncles(block.uncle_relations), # "state_root" => "TODO", "rewards" => prepare_rewards(block.rewards, block, single_block?), - "gas_target_percentage" => gas_target(block), - "gas_used_percentage" => gas_used_percentage(block), - "burnt_fees_percentage" => burnt_fees_percentage(burned_fee, tx_fees), + "gas_target_percentage" => Block.gas_target(block), + "gas_used_percentage" => Block.gas_used_percentage(block), + "burnt_fees_percentage" => burnt_fees_percentage(burnt_fees, transaction_fees), "type" => block |> BlockView.block_type() |> String.downcase(), - "tx_fees" => tx_fees, + "tx_fees" => transaction_fees, "withdrawals_count" => count_withdrawals(block) } + |> chain_type_fields(block, single_block?) end def prepare_rewards(rewards, block, single_block?) do @@ -84,28 +84,11 @@ defmodule BlockScoutWeb.API.V2.BlockView do %{"hash" => uncle_relation.uncle_hash} end - def gas_target(block) do - if Decimal.compare(block.gas_limit, 0) == :gt do - elasticity_multiplier = Application.get_env(:explorer, :elasticity_multiplier) - ratio = Decimal.div(block.gas_used, Decimal.div(block.gas_limit, elasticity_multiplier)) - ratio |> Decimal.sub(1) |> Decimal.mult(100) |> Decimal.to_float() - else - Decimal.new(0) - end - end - - def gas_used_percentage(block) do - if Decimal.compare(block.gas_limit, 0) == :gt do - block.gas_used |> Decimal.div(block.gas_limit) |> Decimal.mult(100) |> Decimal.to_float() - else - Decimal.new(0) - end - end - def burnt_fees_percentage(_, %Decimal{coef: 0}), do: nil - def burnt_fees_percentage(burnt_fees, tx_fees) when not is_nil(tx_fees) and not is_nil(burnt_fees) do - burnt_fees.value |> Decimal.div(tx_fees) |> Decimal.mult(100) |> Decimal.to_float() + def burnt_fees_percentage(burnt_fees, transaction_fees) + when not is_nil(transaction_fees) and not is_nil(burnt_fees) do + burnt_fees |> Decimal.div(transaction_fees) |> Decimal.mult(100) |> Decimal.to_float() end def burnt_fees_percentage(_, _), do: nil @@ -115,4 +98,37 @@ defmodule BlockScoutWeb.API.V2.BlockView do def count_withdrawals(%Block{withdrawals: withdrawals}) when is_list(withdrawals), do: Enum.count(withdrawals) def count_withdrawals(_), do: nil + + case Application.compile_env(:explorer, :chain_type) do + "rsk" -> + defp chain_type_fields(result, block, single_block?) do + if single_block? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.RootstockView.extend_block_json_response(result, block) + else + result + end + end + + "zksync" -> + defp chain_type_fields(result, block, single_block?) do + if single_block? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.ZkSyncView.extend_block_json_response(result, block) + else + result + end + end + + "ethereum" -> + defp chain_type_fields(result, block, single_block?) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.EthereumView.extend_block_json_response(result, block, single_block?) + end + + _ -> + defp chain_type_fields(result, _block, _single_block?) do + result + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex new file mode 100644 index 000000000000..6f7af666458a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex @@ -0,0 +1,49 @@ +defmodule BlockScoutWeb.API.V2.EthereumView do + alias Explorer.Chain.{Block, Transaction} + + defp count_blob_transactions(%Block{transactions: txs}) when is_list(txs), + # EIP-2718 blob transaction type + do: Enum.count(txs, &(&1.type == 3)) + + defp count_blob_transactions(_), do: nil + + def extend_transaction_json_response(out_json, %Transaction{} = transaction) do + case Map.get(transaction, :beacon_blob_transaction) do + nil -> + out_json + + %Ecto.Association.NotLoaded{} -> + out_json + + item -> + out_json + |> Map.put("max_fee_per_blob_gas", item.max_fee_per_blob_gas) + |> Map.put("blob_versioned_hashes", item.blob_versioned_hashes) + |> Map.put("blob_gas_used", item.blob_gas_used) + |> Map.put("blob_gas_price", item.blob_gas_price) + |> Map.put("burnt_blob_fee", Decimal.mult(item.blob_gas_used, item.blob_gas_price)) + end + end + + def extend_block_json_response(out_json, %Block{} = block, single_block?) do + blob_gas_used = Map.get(block, :blob_gas_used) + excess_blob_gas = Map.get(block, :excess_blob_gas) + + extended_out_json = + out_json + |> Map.put("blob_tx_count", count_blob_transactions(block)) + |> Map.put("blob_gas_used", blob_gas_used) + |> Map.put("excess_blob_gas", excess_blob_gas) + + if single_block? do + blob_gas_price = Block.transaction_blob_gas_price(block.transactions) + burnt_blob_transaction_fees = Decimal.mult(blob_gas_used || 0, blob_gas_price || 0) + + extended_out_json + |> Map.put("blob_gas_price", blob_gas_price) + |> Map.put("burnt_blob_fees", burnt_blob_transaction_fees) + else + extended_out_json + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 9b82e01ede77..2839b49439da 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -48,25 +48,36 @@ defmodule BlockScoutWeb.API.V2.Helper do }) end - defp address_with_info(%Address{} = address, _address_hash) do + @doc """ + Gets address with the additional info for api v2 + """ + @spec address_with_info(any(), any()) :: nil | %{optional(<<_::32, _::_*8>>) => any()} + def address_with_info(%Address{} = address, _address_hash) do %{ "hash" => Address.checksum(address), - "is_contract" => is_smart_contract(address), + "is_contract" => Address.smart_contract?(address), "name" => address_name(address), "implementation_name" => implementation_name(address), - "is_verified" => is_verified(address) + "is_verified" => verified?(address), + "ens_domain_name" => address.ens_domain_name } end - defp address_with_info(%NotLoaded{}, address_hash) do + def address_with_info(%{ens_domain_name: name}, address_hash) do + nil + |> address_with_info(address_hash) + |> Map.put("ens_domain_name", name) + end + + def address_with_info(%NotLoaded{}, address_hash) do address_with_info(nil, address_hash) end - defp address_with_info(nil, nil) do + def address_with_info(nil, nil) do nil end - defp address_with_info(_, address_hash) do + def address_with_info(_, address_hash) do %{ "hash" => Address.checksum(address_hash), "is_contract" => false, @@ -94,15 +105,10 @@ defmodule BlockScoutWeb.API.V2.Helper do def implementation_name(_), do: nil - def is_smart_contract(%Address{contract_code: nil}), do: false - def is_smart_contract(%Address{contract_code: _}), do: true - def is_smart_contract(%NotLoaded{}), do: nil - def is_smart_contract(_), do: false - - def is_verified(%Address{smart_contract: nil}), do: false - def is_verified(%Address{smart_contract: %{metadata_from_verified_twin: true}}), do: false - def is_verified(%Address{smart_contract: %NotLoaded{}}), do: nil - def is_verified(%Address{smart_contract: _}), do: true + def verified?(%Address{smart_contract: nil}), do: false + def verified?(%Address{smart_contract: %{metadata_from_verified_twin: true}}), do: false + def verified?(%Address{smart_contract: %NotLoaded{}}), do: nil + def verified?(%Address{smart_contract: _}), do: true def market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value, market_cap_usd: market_cap_usd}) when is_nil(available_supply) or is_nil(usd_value) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex new file mode 100644 index 000000000000..353d09a4e9bd --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -0,0 +1,180 @@ +defmodule BlockScoutWeb.API.V2.OptimismView do + use BlockScoutWeb, :view + + import Ecto.Query, only: [from: 2] + + alias BlockScoutWeb.API.V2.Helper + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.Optimism.Withdrawal + + def render("optimism_txn_batches.json", %{ + batches: batches, + next_page_params: next_page_params + }) do + items = + batches + |> Enum.map(fn batch -> + Task.async(fn -> + tx_count = + Repo.replica().aggregate( + from( + t in Transaction, + inner_join: b in Block, + on: b.hash == t.block_hash and b.consensus == true, + where: t.block_number == ^batch.l2_block_number + ), + :count, + timeout: :infinity + ) + + %{ + "l2_block_number" => batch.l2_block_number, + "tx_count" => tx_count, + "l1_tx_hashes" => batch.frame_sequence.l1_transaction_hashes, + "l1_timestamp" => batch.frame_sequence.l1_timestamp + } + end) + end) + |> Task.yield_many(:infinity) + |> Enum.map(fn {_task, {:ok, item}} -> item end) + + %{ + items: items, + next_page_params: next_page_params + } + end + + def render("optimism_output_roots.json", %{ + roots: roots, + next_page_params: next_page_params + }) do + %{ + items: + Enum.map(roots, fn r -> + %{ + "l2_output_index" => r.l2_output_index, + "l2_block_number" => r.l2_block_number, + "l1_tx_hash" => r.l1_transaction_hash, + "l1_timestamp" => r.l1_timestamp, + "l1_block_number" => r.l1_block_number, + "output_root" => r.output_root + } + end), + next_page_params: next_page_params + } + end + + def render("optimism_deposits.json", %{ + deposits: deposits, + next_page_params: next_page_params + }) do + %{ + items: + Enum.map(deposits, fn deposit -> + %{ + "l1_block_number" => deposit.l1_block_number, + "l2_tx_hash" => deposit.l2_transaction_hash, + "l1_block_timestamp" => deposit.l1_block_timestamp, + "l1_tx_hash" => deposit.l1_transaction_hash, + "l1_tx_origin" => deposit.l1_transaction_origin, + "l2_tx_gas_limit" => deposit.l2_transaction.gas + } + end), + next_page_params: next_page_params + } + end + + def render("optimism_deposits.json", %{deposits: deposits}) do + Enum.map(deposits, fn deposit -> + %{ + "l1_block_number" => deposit.l1_block_number, + "l1_block_timestamp" => deposit.l1_block_timestamp, + "l1_tx_hash" => deposit.l1_transaction_hash, + "l2_tx_hash" => deposit.l2_transaction_hash + } + end) + end + + def render("optimism_withdrawals.json", %{ + withdrawals: withdrawals, + next_page_params: next_page_params, + conn: conn + }) do + %{ + items: + Enum.map(withdrawals, fn w -> + msg_nonce = + Bitwise.band( + Decimal.to_integer(w.msg_nonce), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ) + + msg_nonce_version = Bitwise.bsr(Decimal.to_integer(w.msg_nonce), 240) + + {from_address, from_address_hash} = + with false <- is_nil(w.from), + {:ok, address} <- + Chain.hash_to_address( + w.from, + [necessity_by_association: %{:names => :optional, :smart_contract => :optional}, api?: true], + false + ) do + {address, address.hash} + else + _ -> {nil, nil} + end + + {status, challenge_period_end} = Withdrawal.status(w) + + %{ + "msg_nonce_raw" => Decimal.to_string(w.msg_nonce, :normal), + "msg_nonce" => msg_nonce, + "msg_nonce_version" => msg_nonce_version, + "from" => Helper.address_with_info(conn, from_address, from_address_hash, w.from), + "l2_tx_hash" => w.l2_transaction_hash, + "l2_timestamp" => w.l2_timestamp, + "status" => status, + "l1_tx_hash" => w.l1_transaction_hash, + "challenge_period_end" => challenge_period_end + } + end), + next_page_params: next_page_params + } + end + + def render("optimism_items_count.json", %{count: count}) do + count + end + + def extend_transaction_json_response(out_json, %Transaction{} = transaction) do + out_json + |> add_optional_transaction_field(transaction, :l1_fee) + |> add_optional_transaction_field(transaction, :l1_fee_scalar) + |> add_optional_transaction_field(transaction, :l1_gas_price) + |> add_optional_transaction_field(transaction, :l1_gas_used) + |> add_optimism_fields(transaction.hash) + end + + defp add_optional_transaction_field(out_json, transaction, field) do + case Map.get(transaction, field) do + nil -> out_json + value -> Map.put(out_json, Atom.to_string(field), value) + end + end + + defp add_optimism_fields(out_json, transaction_hash) do + withdrawals = + transaction_hash + |> Withdrawal.transaction_statuses() + |> Enum.map(fn {nonce, status, l1_transaction_hash} -> + %{ + "nonce" => nonce, + "status" => status, + "l1_transaction_hash" => l1_transaction_hash + } + end) + + Map.put(out_json, "op_withdrawals", withdrawals) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex index 3a813a6a1960..3e92f7893ff3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex @@ -1,6 +1,10 @@ defmodule BlockScoutWeb.API.V2.PolygonEdgeView do use BlockScoutWeb, :view + alias BlockScoutWeb.API.V2.Helper + alias Explorer.Chain + alias Explorer.Chain.PolygonEdge.Reader + @spec render(String.t(), map()) :: map() def render("polygon_edge_deposits.json", %{ deposits: deposits, @@ -47,4 +51,47 @@ defmodule BlockScoutWeb.API.V2.PolygonEdgeView do def render("polygon_edge_items_count.json", %{count: count}) do count end + + def extend_transaction_json_response(out_json, tx_hash, connection) do + out_json + |> Map.put("polygon_edge_deposit", polygon_edge_deposit(tx_hash, connection)) + |> Map.put("polygon_edge_withdrawal", polygon_edge_withdrawal(tx_hash, connection)) + end + + defp polygon_edge_deposit(transaction_hash, conn) do + transaction_hash + |> Reader.deposit_by_transaction_hash() + |> polygon_edge_deposit_or_withdrawal(conn) + end + + defp polygon_edge_withdrawal(transaction_hash, conn) do + transaction_hash + |> Reader.withdrawal_by_transaction_hash() + |> polygon_edge_deposit_or_withdrawal(conn) + end + + defp polygon_edge_deposit_or_withdrawal(item, conn) do + if not is_nil(item) do + {from_address, from_address_hash} = hash_to_address_and_hash(item.from) + {to_address, to_address_hash} = hash_to_address_and_hash(item.to) + + item + |> Map.put(:from, Helper.address_with_info(conn, from_address, from_address_hash, item.from)) + |> Map.put(:to, Helper.address_with_info(conn, to_address, to_address_hash, item.to)) + end + end + + defp hash_to_address_and_hash(hash) do + with false <- is_nil(hash), + {:ok, address} <- + Chain.hash_to_address( + hash, + [necessity_by_association: %{:names => :optional, :smart_contract => :optional}, api?: true], + false + ) do + {address, address.hash} + else + _ -> {nil, nil} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex new file mode 100644 index 000000000000..051851bf0e5d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex @@ -0,0 +1,189 @@ +defmodule BlockScoutWeb.API.V2.PolygonZkevmView do + use BlockScoutWeb, :view + + alias Explorer.Chain.Transaction + + @doc """ + Function to render GET requests to `/api/v2/zkevm/batches/:batch_number` endpoint. + """ + @spec render(binary(), map()) :: map() | non_neg_integer() + def render("zkevm_batch.json", %{batch: batch}) do + sequence_tx_hash = + if Map.has_key?(batch, :sequence_transaction) and not is_nil(batch.sequence_transaction) do + batch.sequence_transaction.hash + end + + verify_tx_hash = + if Map.has_key?(batch, :verify_transaction) and not is_nil(batch.verify_transaction) do + batch.verify_transaction.hash + end + + l2_transactions = + if Map.has_key?(batch, :l2_transactions) do + Enum.map(batch.l2_transactions, fn tx -> tx.hash end) + end + + %{ + "number" => batch.number, + "status" => batch_status(batch), + "timestamp" => batch.timestamp, + "transactions" => l2_transactions, + "global_exit_root" => batch.global_exit_root, + "acc_input_hash" => batch.acc_input_hash, + "sequence_tx_hash" => sequence_tx_hash, + "verify_tx_hash" => verify_tx_hash, + "state_root" => batch.state_root + } + end + + @doc """ + Function to render GET requests to `/api/v2/zkevm/batches` endpoint. + """ + def render("zkevm_batches.json", %{ + batches: batches, + next_page_params: next_page_params + }) do + %{ + items: render_zkevm_batches(batches), + next_page_params: next_page_params + } + end + + @doc """ + Function to render GET requests to `/api/v2/main-page/zkevm/batches/confirmed` endpoint. + """ + def render("zkevm_batches.json", %{batches: batches}) do + %{items: render_zkevm_batches(batches)} + end + + @doc """ + Function to render GET requests to `/api/v2/zkevm/batches/count` endpoint. + """ + def render("zkevm_batches_count.json", %{count: count}) do + count + end + + @doc """ + Function to render GET requests to `/api/v2/main-page/zkevm/batches/latest-number` endpoint. + """ + def render("zkevm_batch_latest_number.json", %{number: number}) do + number + end + + @doc """ + Function to render GET requests to `/api/v2/zkevm/deposits` and `/api/v2/zkevm/withdrawals` endpoints. + """ + def render("polygon_zkevm_bridge_items.json", %{ + items: items, + next_page_params: next_page_params + }) do + env = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonZkevm.BridgeL1] + + %{ + items: + Enum.map(items, fn item -> + l1_token = if is_nil(Map.get(item, :l1_token)), do: %{}, else: Map.get(item, :l1_token) + l2_token = if is_nil(Map.get(item, :l2_token)), do: %{}, else: Map.get(item, :l2_token) + + decimals = + cond do + not is_nil(Map.get(l1_token, :decimals)) -> Map.get(l1_token, :decimals) + not is_nil(Map.get(l2_token, :decimals)) -> Map.get(l2_token, :decimals) + true -> env[:native_decimals] + end + + symbol = + cond do + not is_nil(Map.get(l1_token, :symbol)) -> Map.get(l1_token, :symbol) + not is_nil(Map.get(l2_token, :symbol)) -> Map.get(l2_token, :symbol) + true -> env[:native_symbol] + end + + %{ + "block_number" => item.block_number, + "index" => item.index, + "l1_transaction_hash" => item.l1_transaction_hash, + "timestamp" => item.block_timestamp, + "l2_transaction_hash" => item.l2_transaction_hash, + "value" => fractional(Decimal.new(item.amount), Decimal.new(decimals)), + "symbol" => symbol + } + end), + next_page_params: next_page_params + } + end + + @doc """ + Function to render GET requests to `/api/v2/zkevm/deposits/count` and `/api/v2/zkevm/withdrawals/count` endpoints. + """ + def render("polygon_zkevm_bridge_items_count.json", %{count: count}) do + count + end + + defp batch_status(batch) do + sequence_id = Map.get(batch, :sequence_id) + verify_id = Map.get(batch, :verify_id) + + cond do + is_nil(sequence_id) && is_nil(verify_id) -> "Unfinalized" + !is_nil(sequence_id) && is_nil(verify_id) -> "L1 Sequence Confirmed" + !is_nil(verify_id) -> "Finalized" + end + end + + defp fractional(%Decimal{} = amount, %Decimal{} = decimals) do + amount.sign + |> Decimal.new(amount.coef, amount.exp - Decimal.to_integer(decimals)) + |> Decimal.normalize() + |> Decimal.to_string(:normal) + end + + defp render_zkevm_batches(batches) do + Enum.map(batches, fn batch -> + sequence_tx_hash = + if not is_nil(batch.sequence_transaction) do + batch.sequence_transaction.hash + end + + verify_tx_hash = + if not is_nil(batch.verify_transaction) do + batch.verify_transaction.hash + end + + %{ + "number" => batch.number, + "status" => batch_status(batch), + "timestamp" => batch.timestamp, + "tx_count" => batch.l2_transactions_count, + "sequence_tx_hash" => sequence_tx_hash, + "verify_tx_hash" => verify_tx_hash + } + end) + end + + def extend_transaction_json_response(out_json, %Transaction{} = transaction) do + extended_result = + out_json + |> add_optional_transaction_field(transaction, "zkevm_batch_number", :zkevm_batch, :number) + |> add_optional_transaction_field(transaction, "zkevm_sequence_hash", :zkevm_sequence_transaction, :hash) + |> add_optional_transaction_field(transaction, "zkevm_verify_hash", :zkevm_verify_transaction, :hash) + + Map.put(extended_result, "zkevm_status", zkevm_status(extended_result)) + end + + defp zkevm_status(result_map) do + if is_nil(Map.get(result_map, "zkevm_sequence_hash")) do + "Confirmed by Sequencer" + else + "L1 Confirmed" + end + end + + defp add_optional_transaction_field(out_json, transaction, out_field, association, association_field) do + case Map.get(transaction, association) do + nil -> out_json + %Ecto.Association.NotLoaded{} -> out_json + item -> Map.put(out_json, out_field, Map.get(item, association_field)) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/rootstock_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/rootstock_view.ex new file mode 100644 index 000000000000..06d4d8e68f21 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/rootstock_view.ex @@ -0,0 +1,19 @@ +defmodule BlockScoutWeb.API.V2.RootstockView do + alias Explorer.Chain.Block + + def extend_block_json_response(out_json, %Block{} = block) do + out_json + |> add_optional_transaction_field(block, :minimum_gas_price) + |> add_optional_transaction_field(block, :bitcoin_merged_mining_header) + |> add_optional_transaction_field(block, :bitcoin_merged_mining_coinbase_transaction) + |> add_optional_transaction_field(block, :bitcoin_merged_mining_merkle_proof) + |> add_optional_transaction_field(block, :hash_for_merged_mining) + end + + defp add_optional_transaction_field(out_json, block, field) do + case Map.get(block, field) do + nil -> out_json + value -> Map.put(out_json, Atom.to_string(field), value) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index 0376e04f423e..4d9fbea846ad 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.V2.SearchView do alias BlockScoutWeb.{BlockView, Endpoint} alias Explorer.Chain - alias Explorer.Chain.{Address, Block, Hash, Transaction} + alias Explorer.Chain.{Address, Beacon.Blob, Block, Hash, Transaction, UserOperation} def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do %{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params} @@ -47,7 +47,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "name" => search_result.name, "address" => search_result.address_hash, "url" => address_path(Endpoint, :show, search_result.address_hash), - "is_smart_contract_verified" => search_result.verified + "is_smart_contract_verified" => search_result.verified, + "ens_info" => search_result[:ens_info] } end @@ -83,6 +84,26 @@ defmodule BlockScoutWeb.API.V2.SearchView do } end + def prepare_search_result(%{type: "user_operation"} = search_result) do + user_operation_hash = hash_to_string(search_result.user_operation_hash) + + %{ + "type" => search_result.type, + "user_operation_hash" => user_operation_hash, + "timestamp" => search_result.timestamp + } + end + + def prepare_search_result(%{type: "blob"} = search_result) do + blob_hash = hash_to_string(search_result.blob_hash) + + %{ + "type" => search_result.type, + "blob_hash" => blob_hash, + "timestamp" => search_result.timestamp + } + end + defp hash_to_string(%Hash{bytes: bytes}), do: hash_to_string(bytes) defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower) @@ -105,4 +126,12 @@ defmodule BlockScoutWeb.API.V2.SearchView do defp redirect_search_results(%Transaction{} = item) do %{"type" => "transaction", "parameter" => to_string(item.hash)} end + + defp redirect_search_results(%UserOperation{} = item) do + %{"type" => "user_operation", "parameter" => to_string(item.hash)} + end + + defp redirect_search_results(%Blob{} = item) do + %{"type" => "blob", "parameter" => to_string(item.hash)} + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/shibarium_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/shibarium_view.ex new file mode 100644 index 000000000000..b51a27a0a98f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/shibarium_view.ex @@ -0,0 +1,67 @@ +defmodule BlockScoutWeb.API.V2.ShibariumView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.V2.Helper + alias Explorer.Chain + + @spec render(String.t(), map()) :: map() + def render("shibarium_deposits.json", %{ + deposits: deposits, + next_page_params: next_page_params, + conn: conn + }) do + user_addresses = get_user_addresses(deposits, conn) + + %{ + items: + Enum.map(deposits, fn deposit -> + %{ + "l1_block_number" => deposit.l1_block_number, + "l1_transaction_hash" => deposit.l1_transaction_hash, + "l2_transaction_hash" => deposit.l2_transaction_hash, + "user" => Map.get(user_addresses, deposit.user, deposit.user), + "timestamp" => deposit.timestamp + } + end), + next_page_params: next_page_params + } + end + + def render("shibarium_withdrawals.json", %{ + withdrawals: withdrawals, + next_page_params: next_page_params, + conn: conn + }) do + user_addresses = get_user_addresses(withdrawals, conn) + + %{ + items: + Enum.map(withdrawals, fn withdrawal -> + %{ + "l2_block_number" => withdrawal.l2_block_number, + "l2_transaction_hash" => withdrawal.l2_transaction_hash, + "l1_transaction_hash" => withdrawal.l1_transaction_hash, + "user" => Map.get(user_addresses, withdrawal.user, withdrawal.user), + "timestamp" => withdrawal.timestamp + } + end), + next_page_params: next_page_params + } + end + + def render("shibarium_items_count.json", %{count: count}) do + count + end + + defp get_user_addresses(items, conn) do + items + |> Enum.map(& &1.user) + |> Enum.reject(&is_nil(&1)) + |> Enum.uniq() + |> Chain.hashes_to_addresses( + necessity_by_association: %{:names => :optional, :smart_contract => :optional}, + api?: true + ) + |> Enum.into(%{}, &{&1.hash, Helper.address_with_info(conn, &1, &1.hash, true)}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index 08fa9377b0d9..2ae2f7514f32 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -10,7 +10,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do alias BlockScoutWeb.{ABIEncodedValueView, AddressContractView, AddressView} alias Ecto.Changeset alias Explorer.Chain - alias Explorer.Chain.{Address, SmartContract} + alias Explorer.Chain.{Address, SmartContract, SmartContractAdditionalSource} + alias Explorer.Chain.SmartContract.Proxy.EIP1167 alias Explorer.Visualize.Sol2uml require Logger @@ -41,6 +42,18 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do end) end + def render("audit_reports.json", %{reports: reports}) do + %{"items" => Enum.map(reports, &prepare_audit_report/1), "next_page_params" => nil} + end + + defp prepare_audit_report(report) do + %{ + "audit_company_name" => report.audit_company_name, + "audit_report_url" => report.audit_report_url, + "audit_publish_date" => report.audit_publish_date + } + end + def prepare_function_response(outputs, names, contract_address_hash) do case outputs do {:error, %{code: code, message: message, data: data}} -> @@ -134,15 +147,14 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do # credo:disable-for-next-line def prepare_smart_contract(%Address{smart_contract: %SmartContract{} = smart_contract} = address) do - minimal_proxy_template = Chain.get_minimal_proxy_template(address.hash, @api_true) - twin = Chain.get_address_verified_twin_contract(address.hash, @api_true) - metadata_for_verification = minimal_proxy_template || twin.verified_contract + minimal_proxy_template = EIP1167.get_implementation_address(address.hash, @api_true) + bytecode_twin = SmartContract.get_address_verified_twin_contract(address.hash, @api_true) + metadata_for_verification = minimal_proxy_template || bytecode_twin.verified_contract smart_contract_verified = AddressView.smart_contract_verified?(address) - additional_sources_from_twin = twin.additional_sources - fully_verified = Chain.smart_contract_fully_verified?(address.hash, @api_true) + fully_verified = SmartContract.verified_with_full_match?(address.hash, @api_true) additional_sources = - if smart_contract_verified, do: address.smart_contract_additional_sources, else: additional_sources_from_twin + additional_sources(smart_contract, smart_contract_verified, minimal_proxy_template, bytecode_twin) visualize_sol2uml_enabled = Sol2uml.enabled?() target_contract = if smart_contract_verified, do: address.smart_contract, else: metadata_for_verification @@ -182,7 +194,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do if(smart_contract_verified, do: format_constructor_arguments(target_contract.abi, target_contract.constructor_arguments) ), - "language" => smart_contract_language(smart_contract) + "language" => smart_contract_language(smart_contract), + "license_type" => smart_contract.license_type } |> Map.merge(bytecode_info(address)) end @@ -191,6 +204,26 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do bytecode_info(address) end + @doc """ + Returns additional sources of the smart-contract or from bytecode twin or from implementation, if it fits minimal proxy pattern (EIP-1167) + """ + @spec additional_sources(SmartContract.t(), boolean, SmartContract.t() | nil, %{ + :verified_contract => any(), + :additional_sources => SmartContractAdditionalSource.t() | nil + }) :: [SmartContractAdditionalSource.t()] + def additional_sources(smart_contract, smart_contract_verified, minimal_proxy_template, bytecode_twin) do + cond do + !is_nil(minimal_proxy_template) -> + minimal_proxy_template.smart_contract_additional_sources + + smart_contract_verified -> + smart_contract.smart_contract_additional_sources + + true -> + bytecode_twin.additional_sources + end + end + defp bytecode_info(address) do case AddressContractView.contract_creation_code(address) do {:selfdestructed, init} -> @@ -272,7 +305,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do "market_cap" => token && token.circulating_market_cap, "has_constructor_args" => !is_nil(smart_contract.constructor_arguments), "coin_balance" => - if(smart_contract.address.fetched_coin_balance, do: smart_contract.address.fetched_coin_balance.value) + if(smart_contract.address.fetched_coin_balance, do: smart_contract.address.fetched_coin_balance.value), + "license_type" => smart_contract.license_type } end @@ -321,6 +355,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do end def render_json(value, _type) do - value + to_string(value) end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex new file mode 100644 index 000000000000..f713428a3d08 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex @@ -0,0 +1,126 @@ +defmodule BlockScoutWeb.API.V2.StabilityView do + alias BlockScoutWeb.API.V2.{Helper, TokenView} + alias Explorer.Chain.{Hash, Log, Token, Transaction} + + @api_true [api?: true] + @transaction_fee_event_signature "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155" + @transaction_fee_event_abi [ + %{ + "anonymous" => false, + "inputs" => [ + %{ + "indexed" => false, + "internalType" => "address", + "name" => "token", + "type" => "address" + }, + %{ + "indexed" => false, + "internalType" => "uint256", + "name" => "totalFee", + "type" => "uint256" + }, + %{ + "indexed" => false, + "internalType" => "address", + "name" => "validator", + "type" => "address" + }, + %{ + "indexed" => false, + "internalType" => "uint256", + "name" => "validatorFee", + "type" => "uint256" + }, + %{ + "indexed" => false, + "internalType" => "address", + "name" => "dapp", + "type" => "address" + }, + %{ + "indexed" => false, + "internalType" => "uint256", + "name" => "dappFee", + "type" => "uint256" + } + ], + "name" => "TransactionFee", + "type" => "event" + } + ] + + def extend_transaction_json_response(out_json, %Transaction{} = transaction) do + case transaction.transaction_fee_log do + [ + {"token", "address", false, token_address_hash}, + {"totalFee", "uint256", false, total_fee}, + {"validator", "address", false, validator_address_hash}, + {"validatorFee", "uint256", false, validator_fee}, + {"dapp", "address", false, dapp_address_hash}, + {"dappFee", "uint256", false, dapp_fee} + ] -> + stability_fee = %{ + "token" => + TokenView.render("token.json", %{ + token: transaction.transaction_fee_token, + contract_address_hash: bytes_to_address_hash(token_address_hash) + }), + "validator_address" => + Helper.address_with_info(nil, nil, bytes_to_address_hash(validator_address_hash), false), + "dapp_address" => Helper.address_with_info(nil, nil, bytes_to_address_hash(dapp_address_hash), false), + "total_fee" => to_string(total_fee), + "dapp_fee" => to_string(dapp_fee), + "validator_fee" => to_string(validator_fee) + } + + out_json + |> Map.put("stability_fee", stability_fee) + + _ -> + out_json + end + end + + def transform_transactions(transactions) do + do_extend_with_stability_fees_info(transactions) + end + + defp do_extend_with_stability_fees_info(transactions) when is_list(transactions) do + {transactions, _tokens_acc} = + Enum.map_reduce(transactions, %{}, fn transaction, tokens_acc -> + case Log.fetch_log_by_tx_hash_and_first_topic(transaction.hash, @transaction_fee_event_signature, @api_true) do + fee_log when not is_nil(fee_log) -> + {:ok, _selector, mapping} = Log.find_and_decode(@transaction_fee_event_abi, fee_log, transaction.hash) + + [{"token", "address", false, token_address_hash}, _, _, _, _, _] = mapping + + {token, new_tokens_acc} = check_tokens_acc(bytes_to_address_hash(token_address_hash), tokens_acc) + + {%Transaction{transaction | transaction_fee_log: mapping, transaction_fee_token: token}, new_tokens_acc} + + _ -> + {transaction, tokens_acc} + end + end) + + transactions + end + + defp do_extend_with_stability_fees_info(transaction) do + [transaction] = do_extend_with_stability_fees_info([transaction]) + transaction + end + + defp check_tokens_acc(token_address_hash, tokens_acc) do + if Map.has_key?(tokens_acc, token_address_hash) do + {tokens_acc[token_address_hash], tokens_acc} + else + token = Token.get_by_contract_address_hash(token_address_hash, @api_true) + + {token, Map.put(tokens_acc, token_address_hash, token)} + end + end + + defp bytes_to_address_hash(bytes), do: %Hash{byte_count: 20, bytes: bytes} +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex new file mode 100644 index 000000000000..8bbee60b6f1c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex @@ -0,0 +1,130 @@ +defmodule BlockScoutWeb.API.V2.SuaveView do + alias BlockScoutWeb.API.V2.Helper, as: APIHelper + alias BlockScoutWeb.API.V2.TransactionView + + alias Explorer.Helper, as: ExplorerHelper + + alias Ecto.Association.NotLoaded + alias Explorer.Chain.{Hash, Transaction} + + @suave_bid_event "0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e" + + def extend_transaction_json_response(%Transaction{} = transaction, out_json, single_tx?, conn, watchlist_names) do + if is_nil(Map.get(transaction, :execution_node_hash)) do + out_json + else + wrapped_to_address = Map.get(transaction, :wrapped_to_address) + wrapped_to_address_hash = Map.get(transaction, :wrapped_to_address_hash) + wrapped_input = Map.get(transaction, :wrapped_input) + wrapped_hash = Map.get(transaction, :wrapped_hash) + execution_node = Map.get(transaction, :execution_node) + execution_node_hash = Map.get(transaction, :execution_node_hash) + wrapped_type = Map.get(transaction, :wrapped_type) + wrapped_nonce = Map.get(transaction, :wrapped_nonce) + wrapped_gas = Map.get(transaction, :wrapped_gas) + wrapped_gas_price = Map.get(transaction, :wrapped_gas_price) + wrapped_max_priority_fee_per_gas = Map.get(transaction, :wrapped_max_priority_fee_per_gas) + wrapped_max_fee_per_gas = Map.get(transaction, :wrapped_max_fee_per_gas) + wrapped_value = Map.get(transaction, :wrapped_value) + + {[wrapped_decoded_input], _, _} = + TransactionView.decode_transactions( + [ + %Transaction{ + to_address: wrapped_to_address, + input: wrapped_input, + hash: wrapped_hash + } + ], + false + ) + + out_json + |> Map.put("allowed_peekers", suave_parse_allowed_peekers(transaction.logs)) + |> Map.put( + "execution_node", + APIHelper.address_with_info( + conn, + execution_node, + execution_node_hash, + single_tx?, + watchlist_names + ) + ) + |> Map.put("wrapped", %{ + "type" => wrapped_type, + "nonce" => wrapped_nonce, + "to" => + APIHelper.address_with_info( + conn, + wrapped_to_address, + wrapped_to_address_hash, + single_tx?, + watchlist_names + ), + "gas_limit" => wrapped_gas, + "gas_price" => wrapped_gas_price, + "fee" => + TransactionView.format_fee( + Transaction.fee( + %Transaction{gas: wrapped_gas, gas_price: wrapped_gas_price, gas_used: nil}, + :wei + ) + ), + "max_priority_fee_per_gas" => wrapped_max_priority_fee_per_gas, + "max_fee_per_gas" => wrapped_max_fee_per_gas, + "value" => wrapped_value, + "hash" => wrapped_hash, + "method" => + TransactionView.method_name( + %Transaction{to_address: wrapped_to_address, input: wrapped_input}, + wrapped_decoded_input + ), + "decoded_input" => TransactionView.decoded_input(wrapped_decoded_input), + "raw_input" => wrapped_input + }) + end + end + + # @spec suave_parse_allowed_peekers(Ecto.Schema.has_many(Log.t())) :: [String.t()] + defp suave_parse_allowed_peekers(%NotLoaded{}), do: [] + + defp suave_parse_allowed_peekers(logs) do + suave_bid_contracts = + Application.get_all_env(:explorer)[Transaction][:suave_bid_contracts] + |> String.split(",") + |> Enum.map(fn sbc -> String.downcase(String.trim(sbc)) end) + + bid_event = + Enum.find(logs, fn log -> + sanitize_log_first_topic(log.first_topic) == @suave_bid_event && + Enum.member?(suave_bid_contracts, String.downcase(Hash.to_string(log.address_hash))) + end) + + if is_nil(bid_event) do + [] + else + [_bid_id, _decryption_condition, allowed_peekers] = + ExplorerHelper.decode_data(bid_event.data, [{:bytes, 16}, {:uint, 64}, {:array, :address}]) + + Enum.map(allowed_peekers, fn peeker -> + "0x" <> Base.encode16(peeker, case: :lower) + end) + end + end + + defp sanitize_log_first_topic(first_topic) do + if is_nil(first_topic) do + "" + else + sanitized = + if is_binary(first_topic) do + first_topic + else + Hash.to_string(first_topic) + end + + String.downcase(sanitized) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index f78a3340d31b..f1ed2d99537c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -1,12 +1,13 @@ defmodule BlockScoutWeb.API.V2.TokenView do + use BlockScoutWeb, :view + alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.NFTHelper - alias Explorer.Chain - alias Explorer.Chain.Address - - @api_true [api?: true] + alias Ecto.Association.NotLoaded + alias Explorer.Chain.{Address, BridgedToken} + alias Explorer.Chain.Token.Instance - def render("token.json", %{token: nil, contract_address_hash: contract_address_hash}) do + def render("token.json", %{token: nil = token, contract_address_hash: contract_address_hash}) do %{ "address" => Address.checksum(contract_address_hash), "symbol" => nil, @@ -19,6 +20,11 @@ defmodule BlockScoutWeb.API.V2.TokenView do "icon_url" => nil, "circulating_market_cap" => nil } + |> maybe_append_bridged_info(token) + end + + def render("token.json", %{token: nil}) do + nil end def render("token.json", %{token: token}) do @@ -30,10 +36,12 @@ defmodule BlockScoutWeb.API.V2.TokenView do "type" => token.type, "holders" => prepare_holders_count(token.holder_count), "exchange_rate" => exchange_rate(token), + "volume_24h" => token.volume_24h, "total_supply" => token.total_supply, "icon_url" => token.icon_url, "circulating_market_cap" => token.circulating_market_cap } + |> maybe_append_bridged_info(token) end def render("token_balances.json", %{ @@ -66,6 +74,20 @@ defmodule BlockScoutWeb.API.V2.TokenView do } end + def render("bridged_tokens.json", %{tokens: tokens, next_page_params: next_page_params}) do + %{"items" => Enum.map(tokens, &render("bridged_token.json", %{token: &1})), "next_page_params" => next_page_params} + end + + def render("bridged_token.json", %{token: {token, bridged_token}}) do + "token.json" + |> render(%{token: token}) + |> Map.merge(%{ + foreign_address: Address.checksum(bridged_token.foreign_token_contract_address_hash), + bridge_type: bridged_token.type, + origin_chain_id: bridged_token.foreign_chain_id + }) + end + def exchange_rate(%{fiat_value: fiat_value}) when not is_nil(fiat_value), do: to_string(fiat_value) def exchange_rate(_), do: nil @@ -78,25 +100,43 @@ defmodule BlockScoutWeb.API.V2.TokenView do } end + @doc """ + Internal json rendering function + """ def prepare_token_instance(instance, token) do - is_unique = - not (token.type == "ERC-1155") or - Chain.token_id_1155_is_unique?(token.contract_address_hash, instance.token_id, @api_true) - %{ "id" => instance.token_id, "metadata" => instance.metadata, - "owner" => - if(is_unique, do: instance.owner && Helper.address_with_info(nil, instance.owner, instance.owner.hash, false)), + "owner" => token_instance_owner(instance.is_unique, instance), "token" => render("token.json", %{token: token}), "external_app_url" => NFTHelper.external_url(instance), "animation_url" => instance.metadata && NFTHelper.retrieve_image(instance.metadata["animation_url"]), "image_url" => instance.metadata && NFTHelper.get_media_src(instance.metadata, false), - "is_unique" => is_unique + "is_unique" => instance.is_unique } end + defp token_instance_owner(false, _instance), do: nil + defp token_instance_owner(nil, _instance), do: nil + + defp token_instance_owner(_is_unique, %Instance{owner: %NotLoaded{}} = instance), + do: Helper.address_with_info(nil, nil, instance.owner_address_hash, false) + + defp token_instance_owner(_is_unique, %Instance{owner: nil} = instance), + do: Helper.address_with_info(nil, nil, instance.owner_address_hash, false) + + defp token_instance_owner(_is_unique, instance), + do: instance.owner && Helper.address_with_info(nil, instance.owner, instance.owner.hash, false) + defp prepare_holders_count(nil), do: nil defp prepare_holders_count(count) when count < 0, do: prepare_holders_count(0) defp prepare_holders_count(count), do: to_string(count) + + defp maybe_append_bridged_info(map, token) do + if BridgedToken.enabled?() do + (token && Map.put(map, "is_bridged", token.bridged || false)) || map + else + map + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index c3e4d8288740..2c4c2fc47811 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -2,25 +2,22 @@ defmodule BlockScoutWeb.API.V2.TransactionView do use BlockScoutWeb, :view alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} + alias BlockScoutWeb.{ABIEncodedValueView, TransactionView} alias BlockScoutWeb.Models.GetTransactionTags alias BlockScoutWeb.Tokens.Helper, as: TokensHelper alias BlockScoutWeb.TransactionStateView alias Ecto.Association.NotLoaded alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, Log, Token, Transaction, Wei} + alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, Transaction, Wei} alias Explorer.Chain.Block.Reward - alias Explorer.Chain.PolygonEdge.Reader alias Explorer.Chain.Transaction.StateChange alias Explorer.Counters.AverageBlockTime alias Timex.Duration import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - import Explorer.Chain.Transaction, only: [maybe_prepare_stability_fees: 1, bytes_to_address_hash: 1] - import Explorer.Helper, only: [decode_data: 2] @api_true [api?: true] - @suave_bid_event "0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e" def render("message.json", assigns) do ApiView.render("message.json", assigns) @@ -38,7 +35,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do %{ "items" => transactions - |> maybe_prepare_stability_fees() + |> chain_type_transformations() |> Enum.zip(decoded_transactions) |> Enum.map(fn {tx, decoded_input} -> prepare_transaction(tx, conn, false, block_height, watchlist_names, decoded_input) @@ -56,7 +53,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do {decoded_transactions, _, _} = decode_transactions(transactions, true) transactions - |> maybe_prepare_stability_fees() + |> chain_type_transformations() |> Enum.zip(decoded_transactions) |> Enum.map(fn {tx, decoded_input} -> prepare_transaction(tx, conn, false, block_height, watchlist_names, decoded_input) @@ -70,7 +67,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do %{ "items" => transactions - |> maybe_prepare_stability_fees() + |> chain_type_transformations() |> Enum.zip(decoded_transactions) |> Enum.map(fn {tx, decoded_input} -> prepare_transaction(tx, conn, false, block_height, decoded_input) end), "next_page_params" => next_page_params @@ -88,7 +85,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do {decoded_transactions, _, _} = decode_transactions(transactions, true) transactions - |> maybe_prepare_stability_fees() + |> chain_type_transformations() |> Enum.zip(decoded_transactions) |> Enum.map(fn {tx, decoded_input} -> prepare_transaction(tx, conn, false, block_height, decoded_input) end) end @@ -96,7 +93,10 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transaction.json", %{transaction: transaction, conn: conn}) do block_height = Chain.block_height(@api_true) {[decoded_input], _, _} = decode_transactions([transaction], false) - prepare_transaction(transaction |> maybe_prepare_stability_fees(), conn, true, block_height, decoded_input) + + transaction + |> chain_type_transformations() + |> prepare_transaction(conn, true, block_height, decoded_input) end def render("raw_trace.json", %{internal_transactions: internal_transactions}) do @@ -184,6 +184,24 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end + def render("stats.json", %{ + transactions_count_24h: transactions_count, + pending_transactions_count: pending_transactions_count, + transaction_fees_sum_24h: transaction_fees_sum, + transaction_fees_avg_24h: transaction_fees_avg + }) do + %{ + "transactions_count_24h" => transactions_count, + "pending_transactions_count" => pending_transactions_count, + "transaction_fees_sum_24h" => transaction_fees_sum, + "transaction_fees_avg_24h" => transaction_fees_avg + } + end + + @doc """ + Decodes list of logs + """ + @spec decode_logs([Log.t()], boolean) :: [tuple] def decode_logs(logs, skip_sig_provider?) do {result, _, _} = Enum.reduce(logs, {[], %{}, %{}}, fn log, {results, contracts_acc, events_acc} -> @@ -204,12 +222,15 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def decode_transactions(transactions, skip_sig_provider?) do - Enum.reduce(transactions, {[], %{}, %{}}, fn transaction, {results, abi_acc, methods_acc} -> - {result, abi_acc, methods_acc} = - Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true, abi_acc, methods_acc) + {results, abi_acc, methods_acc} = + Enum.reduce(transactions, {[], %{}, %{}}, fn transaction, {results, abi_acc, methods_acc} -> + {result, abi_acc, methods_acc} = + Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true, abi_acc, methods_acc) - {Enum.reverse([format_decoded_input(result) | Enum.reverse(results)]), abi_acc, methods_acc} - end) + {[format_decoded_input(result) | results], abi_acc, methods_acc} + end) + + {Enum.reverse(results), abi_acc, methods_acc} end def prepare_token_transfer(token_transfer, _conn, decoded_input) do @@ -239,16 +260,25 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end + # credo:disable-for-next-line /Complexity/ def prepare_token_transfer_total(token_transfer) do case TokensHelper.token_transfer_amount_for_api(token_transfer) do {:ok, :erc721_instance} -> - %{"token_id" => List.first(token_transfer.token_ids)} + %{"token_id" => token_transfer.token_ids && List.first(token_transfer.token_ids)} {:ok, :erc1155_instance, value, decimals} -> - %{"token_id" => List.first(token_transfer.token_ids), "value" => value, "decimals" => decimals} + %{ + "token_id" => token_transfer.token_ids && List.first(token_transfer.token_ids), + "value" => value, + "decimals" => decimals + } {:ok, :erc1155_instance, values, token_ids, decimals} -> - %{"token_id" => List.first(token_ids), "value" => List.first(values), "decimals" => decimals} + %{ + "token_id" => token_ids && List.first(token_ids), + "value" => values && List.first(values), + "decimals" => decimals + } {:ok, value, decimals} -> %{"value" => value, "decimals" => decimals} @@ -283,12 +313,12 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end - def prepare_log(log, transaction_or_hash, decoded_log) do + def prepare_log(log, transaction_or_hash, decoded_log, tags_for_address_needed? \\ false) do decoded = process_decoded_log(decoded_log) %{ "tx_hash" => get_tx_hash(transaction_or_hash), - "address" => Helper.address_with_info(nil, log.address, log.address_hash, false), + "address" => Helper.address_with_info(nil, log.address, log.address_hash, tags_for_address_needed?), "topics" => [ log.first_topic, log.second_topic, @@ -353,9 +383,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do max_priority_fee_per_gas = transaction.max_priority_fee_per_gas max_fee_per_gas = transaction.max_fee_per_gas - priority_fee_per_gas = priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) + priority_fee_per_gas = Transaction.priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) - burned_fee = burned_fee(transaction, max_fee_per_gas, base_fee_per_gas) + burnt_fees = burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) status = transaction |> Chain.transaction_to_status() |> format_status() @@ -368,7 +398,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "result" => status, "status" => transaction.status, "block" => transaction.block_number, - "timestamp" => block_timestamp(transaction.block), + "timestamp" => block_timestamp(transaction), "from" => Helper.address_with_info( single_tx? && conn, @@ -396,8 +426,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "confirmations" => transaction.block |> Chain.confirmations(block_height: block_height) |> format_confirmations(), "confirmation_duration" => processing_time_duration(transaction), "value" => transaction.value, - "fee" => transaction |> Chain.fee(:wei) |> format_fee(), - "gas_price" => transaction.gas_price, + "fee" => transaction |> Transaction.fee(:wei) |> format_fee(), + "gas_price" => transaction.gas_price || Transaction.effective_gas_price(transaction), "type" => transaction.type, "gas_used" => transaction.gas_used, "gas_limit" => transaction.gas, @@ -405,7 +435,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "max_priority_fee_per_gas" => transaction.max_priority_fee_per_gas, "base_fee_per_gas" => base_fee_per_gas, "priority_fee" => priority_fee_per_gas && Wei.mult(priority_fee_per_gas, transaction.gas_used), - "tx_burnt_fee" => burned_fee, + "tx_burnt_fee" => burnt_fees, "nonce" => transaction.nonce, "position" => transaction.index, "revert_reason" => revert_reason, @@ -423,138 +453,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do result |> chain_type_fields(transaction, single_tx?, conn, watchlist_names) - |> maybe_put_stability_fee(transaction) - end - - defp chain_type_fields(result, transaction, single_tx?, conn, watchlist_names) do - case single_tx? && Application.get_env(:explorer, :chain_type) do - "polygon_edge" -> - result - |> Map.put("polygon_edge_deposit", polygon_edge_deposit(transaction.hash, conn)) - |> Map.put("polygon_edge_withdrawal", polygon_edge_withdrawal(transaction.hash, conn)) - - "polygon_zkevm" -> - extended_result = - result - |> add_optional_transaction_field(transaction, "zkevm_batch_number", :zkevm_batch, :number) - |> add_optional_transaction_field(transaction, "zkevm_sequence_hash", :zkevm_sequence_transaction, :hash) - |> add_optional_transaction_field(transaction, "zkevm_verify_hash", :zkevm_verify_transaction, :hash) - - Map.put(extended_result, "zkevm_status", zkevm_status(extended_result)) - - "suave" -> - suave_fields(transaction, result, single_tx?, conn, watchlist_names) - - _ -> - result - end - end - - defp add_optional_transaction_field(result, transaction, field_name, assoc_name, assoc_field) do - case Map.get(transaction, assoc_name) do - nil -> result - %Ecto.Association.NotLoaded{} -> result - item -> Map.put(result, field_name, Map.get(item, assoc_field)) - end - end - - defp zkevm_status(result_map) do - if is_nil(Map.get(result_map, "zkevm_sequence_hash")) do - "Confirmed by Sequencer" - else - "L1 Confirmed" - end - end - - defp suave_fields(transaction, result, single_tx?, conn, watchlist_names) do - if is_nil(transaction.execution_node_hash) do - result - else - {[wrapped_decoded_input], _, _} = - decode_transactions( - [ - %Transaction{ - to_address: transaction.wrapped_to_address, - input: transaction.wrapped_input, - hash: transaction.wrapped_hash - } - ], - false - ) - - result - |> Map.put("allowed_peekers", suave_parse_allowed_peekers(transaction.logs)) - |> Map.put( - "execution_node", - Helper.address_with_info( - single_tx? && conn, - transaction.execution_node, - transaction.execution_node_hash, - single_tx?, - watchlist_names - ) - ) - |> Map.put("wrapped", %{ - "type" => transaction.wrapped_type, - "nonce" => transaction.wrapped_nonce, - "to" => - Helper.address_with_info( - single_tx? && conn, - transaction.wrapped_to_address, - transaction.wrapped_to_address_hash, - single_tx?, - watchlist_names - ), - "gas_limit" => transaction.wrapped_gas, - "gas_price" => transaction.wrapped_gas_price, - "fee" => - format_fee( - Chain.fee( - %Transaction{gas: transaction.wrapped_gas, gas_price: transaction.wrapped_gas_price, gas_used: nil}, - :wei - ) - ), - "max_priority_fee_per_gas" => transaction.wrapped_max_priority_fee_per_gas, - "max_fee_per_gas" => transaction.wrapped_max_fee_per_gas, - "value" => transaction.wrapped_value, - "hash" => transaction.wrapped_hash, - "method" => - method_name( - %Transaction{to_address: transaction.wrapped_to_address, input: transaction.wrapped_input}, - wrapped_decoded_input - ), - "decoded_input" => decoded_input(wrapped_decoded_input), - "raw_input" => transaction.wrapped_input - }) - end - end - - defp suave_parse_allowed_peekers(logs) do - suave_bid_contracts = - Application.get_all_env(:explorer)[Transaction][:suave_bid_contracts] - |> String.split(",") - |> Enum.map(fn sbc -> String.downcase(String.trim(sbc)) end) - - bid_event = - Enum.find(logs, fn log -> - sanitize_log_first_topic(log.first_topic) == @suave_bid_event && - Enum.member?(suave_bid_contracts, String.downcase(Hash.to_string(log.address_hash))) - end) - - if is_nil(bid_event) do - [] - else - [_bid_id, _decryption_condition, allowed_peekers] = - decode_data(bid_event.data, [{:bytes, 16}, {:uint, 64}, {:array, :address}]) - - Enum.map(allowed_peekers, fn peeker -> - "0x" <> Base.encode16(peeker, case: :lower) - end) - end - end - - defp sanitize_log_first_topic(first_topic) do - if is_nil(first_topic), do: "", else: String.downcase(first_topic) end def token_transfers(_, _conn, false), do: nil @@ -573,22 +471,16 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def token_transfers_overflow(token_transfers, _), do: Enum.count(token_transfers) > Chain.get_token_transfers_per_transaction_preview_count() - defp transaction_actions(%NotLoaded{}), do: [] + def transaction_actions(%NotLoaded{}), do: [] - defp transaction_actions(actions) do + @doc """ + Renders transaction actions + """ + def transaction_actions(actions) do render("transaction_actions.json", %{actions: actions}) end - defp priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) do - if is_nil(max_priority_fee_per_gas) or is_nil(base_fee_per_gas), - do: nil, - else: - Enum.min_by([max_priority_fee_per_gas, Wei.sub(max_fee_per_gas, base_fee_per_gas)], fn x -> - Wei.to(x, :wei) - end) - end - - defp burned_fee(transaction, max_fee_per_gas, base_fee_per_gas) do + defp burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) do if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do if Decimal.compare(max_fee_per_gas.value, 0) == :eq do %Wei{value: Decimal.new(0)} @@ -617,7 +509,11 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end end - defp decoded_input(decoded_input) do + @doc """ + Prepares decoded tx info + """ + @spec decoded_input(any()) :: map() | nil + def decoded_input(decoded_input) do case decoded_input do {:ok, method_id, text, mapping} -> render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: false) @@ -642,13 +538,13 @@ defmodule BlockScoutWeb.API.V2.TransactionView do defp format_status({:error, reason}), do: reason defp format_status(status), do: status - defp format_decoded_input({:error, _, []}), do: nil - defp format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0) - defp format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded - defp format_decoded_input(_), do: nil + @spec format_decoded_input(any()) :: nil | map() | tuple() + def format_decoded_input({:error, _, []}), do: nil + def format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0) + def format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded + def format_decoded_input(_), do: nil defp format_decoded_log_input({:error, :could_not_decode}), do: nil - defp format_decoded_log_input({:error, :no_matching_function}), do: nil defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded defp format_decoded_log_input({:error, _, candidates}), do: Enum.at(candidates, 0) @@ -696,31 +592,64 @@ defmodule BlockScoutWeb.API.V2.TransactionView do |> Timex.diff(right, :milliseconds) end - defp method_name(_, _, skip_sc_check? \\ false) + @doc """ + Return method name used in tx + """ + @spec method_name(Transaction.t(), any(), boolean()) :: binary() | nil + def method_name(_, _, skip_sc_check? \\ false) - defp method_name(_, {:ok, _method_id, text, _mapping}, _) do + def method_name(_, {:ok, _method_id, text, _mapping}, _) do Transaction.parse_method_name(text, false) end - defp method_name( - %Transaction{to_address: to_address, input: %{bytes: <>}}, - _, - skip_sc_check? - ) do - if skip_sc_check? || Helper.is_smart_contract(to_address) do + def method_name( + %Transaction{to_address: to_address, input: %{bytes: <>}}, + _, + skip_sc_check? + ) do + if skip_sc_check? || Address.smart_contract?(to_address) do "0x" <> Base.encode16(method_id, case: :lower) else nil end end - defp method_name(_, _, _) do + def method_name(_, _, _) do nil end - defp tx_types(tx, types \\ [], stage \\ :token_transfer) + @doc """ + Returns array of token types for tx. + """ + @spec tx_types( + Explorer.Chain.Transaction.t(), + [tx_type], + tx_type + ) :: [tx_type] + when tx_type: + :coin_transfer + | :contract_call + | :contract_creation + | :rootstock_bridge + | :rootstock_remasc + | :token_creation + | :token_transfer + | :blob_transaction + def tx_types(tx, types \\ [], stage \\ :blob_transaction) + + def tx_types(%Transaction{type: type} = tx, types, :blob_transaction) do + # EIP-2718 blob transaction type + types = + if type == 3 do + [:blob_transaction | types] + else + types + end + + tx_types(tx, types, :token_transfer) + end - defp tx_types(%Transaction{token_transfers: token_transfers} = tx, types, :token_transfer) do + def tx_types(%Transaction{token_transfers: token_transfers} = tx, types, :token_transfer) do types = if (!is_nil(token_transfers) && token_transfers != [] && !match?(%NotLoaded{}, token_transfers)) || tx.has_token_transfers do @@ -732,7 +661,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :token_creation) end - defp tx_types(%Transaction{created_contract_address: created_contract_address} = tx, types, :token_creation) do + def tx_types(%Transaction{created_contract_address: created_contract_address} = tx, types, :token_creation) do types = if match?(%Address{}, created_contract_address) && match?(%Token{}, created_contract_address.token) do [:token_creation | types] @@ -743,11 +672,11 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :contract_creation) end - defp tx_types( - %Transaction{to_address_hash: to_address_hash} = tx, - types, - :contract_creation - ) do + def tx_types( + %Transaction{to_address_hash: to_address_hash} = tx, + types, + :contract_creation + ) do types = if is_nil(to_address_hash) do [:contract_creation | types] @@ -758,9 +687,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :contract_call) end - defp tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do + def tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do types = - if Helper.is_smart_contract(to_address) do + if Address.smart_contract?(to_address) do [:contract_call | types] else types @@ -769,7 +698,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :coin_transfer) end - defp tx_types(%Transaction{value: value} = tx, types, :coin_transfer) do + def tx_types(%Transaction{value: value} = tx, types, :coin_transfer) do types = if Decimal.compare(value.value, 0) == :gt do [:coin_transfer | types] @@ -780,9 +709,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :rootstock_remasc) end - defp tx_types(tx, types, :rootstock_remasc) do + def tx_types(tx, types, :rootstock_remasc) do types = - if Transaction.is_rootstock_remasc_transaction(tx) do + if Transaction.rootstock_remasc_transaction?(tx) do [:rootstock_remasc | types] else types @@ -791,14 +720,15 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :rootstock_bridge) end - defp tx_types(tx, types, :rootstock_bridge) do - if Transaction.is_rootstock_bridge_transaction(tx) do + def tx_types(tx, types, :rootstock_bridge) do + if Transaction.rootstock_bridge_transaction?(tx) do [:rootstock_bridge | types] else types end end + defp block_timestamp(%Transaction{block_timestamp: block_ts}) when not is_nil(block_ts), do: block_ts defp block_timestamp(%Transaction{block: %Block{} = block}), do: block.timestamp defp block_timestamp(%Block{} = block), do: block.timestamp defp block_timestamp(_), do: nil @@ -853,71 +783,111 @@ defmodule BlockScoutWeb.API.V2.TransactionView do Map.merge(map, %{"change" => change}) end - defp polygon_edge_deposit(transaction_hash, conn) do - transaction_hash - |> Reader.deposit_by_transaction_hash() - |> polygon_edge_deposit_or_withdrawal(conn) - end + case Application.compile_env(:explorer, :chain_type) do + "polygon_edge" -> + defp chain_type_transformations(transactions) do + transactions + end - defp polygon_edge_withdrawal(transaction_hash, conn) do - transaction_hash - |> Reader.withdrawal_by_transaction_hash() - |> polygon_edge_deposit_or_withdrawal(conn) - end + defp chain_type_fields(result, transaction, single_tx?, conn, _watchlist_names) do + if single_tx? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.PolygonEdgeView.extend_transaction_json_response(result, transaction.hash, conn) + else + result + end + end - defp polygon_edge_deposit_or_withdrawal(item, conn) do - if not is_nil(item) do - {from_address, from_address_hash} = hash_to_address_and_hash(item.from) - {to_address, to_address_hash} = hash_to_address_and_hash(item.to) + "polygon_zkevm" -> + defp chain_type_transformations(transactions) do + transactions + end - item - |> Map.put(:from, Helper.address_with_info(conn, from_address, from_address_hash, item.from)) - |> Map.put(:to, Helper.address_with_info(conn, to_address, to_address_hash, item.to)) - end - end + defp chain_type_fields(result, transaction, single_tx?, _conn, _watchlist_names) do + if single_tx? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.PolygonZkevmView.extend_transaction_json_response(result, transaction) + else + result + end + end - defp hash_to_address_and_hash(hash) do - with false <- is_nil(hash), - {:ok, address} <- - Chain.hash_to_address( - hash, - [necessity_by_association: %{:names => :optional, :smart_contract => :optional}, api?: true], - false - ) do - {address, address.hash} - else - _ -> {nil, nil} - end - end + "zksync" -> + defp chain_type_transformations(transactions) do + transactions + end - defp maybe_put_stability_fee(body, transaction) do - with "stability" <- Application.get_env(:explorer, :chain_type), - [ - {"token", "address", false, token_address_hash}, - {"totalFee", "uint256", false, total_fee}, - {"validator", "address", false, validator_address_hash}, - {"validatorFee", "uint256", false, validator_fee}, - {"dapp", "address", false, dapp_address_hash}, - {"dappFee", "uint256", false, dapp_fee} - ] <- transaction.transaction_fee_log do - stability_fee = %{ - "token" => - TokenView.render("token.json", %{ - token: transaction.transaction_fee_token, - contract_address_hash: bytes_to_address_hash(token_address_hash) - }), - "validator_address" => Helper.address_with_info(nil, nil, bytes_to_address_hash(validator_address_hash), false), - "dapp_address" => Helper.address_with_info(nil, nil, bytes_to_address_hash(dapp_address_hash), false), - "total_fee" => to_string(total_fee), - "dapp_fee" => to_string(dapp_fee), - "validator_fee" => to_string(validator_fee) - } - - body - |> Map.put("stability_fee", stability_fee) - else - _ -> - body - end + defp chain_type_fields(result, transaction, single_tx?, _conn, _watchlist_names) do + if single_tx? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.ZkSyncView.extend_transaction_json_response(result, transaction) + else + result + end + end + + "optimism" -> + defp chain_type_transformations(transactions) do + transactions + end + + defp chain_type_fields(result, transaction, single_tx?, _conn, _watchlist_names) do + if single_tx? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.OptimismView.extend_transaction_json_response(result, transaction) + else + result + end + end + + "suave" -> + defp chain_type_transformations(transactions) do + transactions + end + + defp chain_type_fields(result, transaction, single_tx?, conn, watchlist_names) do + if single_tx? do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.SuaveView.extend_transaction_json_response( + transaction, + result, + single_tx?, + conn, + watchlist_names + ) + else + result + end + end + + "stability" -> + defp chain_type_transformations(transactions) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.StabilityView.transform_transactions(transactions) + end + + defp chain_type_fields(result, transaction, _single_tx?, _conn, _watchlist_names) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.StabilityView.extend_transaction_json_response(result, transaction) + end + + "ethereum" -> + defp chain_type_transformations(transactions) do + transactions + end + + defp chain_type_fields(result, transaction, _single_tx?, _conn, _watchlist_names) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.EthereumView.extend_transaction_json_response(result, transaction) + end + + _ -> + defp chain_type_transformations(transactions) do + transactions + end + + defp chain_type_fields(result, _transaction, _single_tx?, _conn, _watchlist_names) do + result + end end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex new file mode 100644 index 000000000000..e5b719557b95 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex @@ -0,0 +1,17 @@ +defmodule BlockScoutWeb.API.V2.ValidatorView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.V2.Helper + + def render("stability_validators.json", %{validators: validators, next_page_params: next_page_params}) do + %{"items" => Enum.map(validators, &prepare_validator(&1)), "next_page_params" => next_page_params} + end + + defp prepare_validator(validator) do + %{ + "address" => Helper.address_with_info(nil, validator.address, validator.address_hash, true), + "state" => validator.state, + "blocks_validated_count" => validator.blocks_validated + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zkevm_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zkevm_view.ex deleted file mode 100644 index a4b0eb2b0c50..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zkevm_view.ex +++ /dev/null @@ -1,104 +0,0 @@ -defmodule BlockScoutWeb.API.V2.ZkevmView do - use BlockScoutWeb, :view - - @doc """ - Function to render GET requests to `/api/v2/zkevm/batches/:batch_number` endpoint. - """ - @spec render(binary(), map()) :: map() | non_neg_integer() - def render("zkevm_batch.json", %{batch: batch}) do - sequence_tx_hash = - if Map.has_key?(batch, :sequence_transaction) and not is_nil(batch.sequence_transaction) do - batch.sequence_transaction.hash - end - - verify_tx_hash = - if Map.has_key?(batch, :verify_transaction) and not is_nil(batch.verify_transaction) do - batch.verify_transaction.hash - end - - l2_transactions = - if Map.has_key?(batch, :l2_transactions) do - Enum.map(batch.l2_transactions, fn tx -> tx.hash end) - end - - %{ - "number" => batch.number, - "status" => batch_status(batch), - "timestamp" => batch.timestamp, - "transactions" => l2_transactions, - "global_exit_root" => batch.global_exit_root, - "acc_input_hash" => batch.acc_input_hash, - "sequence_tx_hash" => sequence_tx_hash, - "verify_tx_hash" => verify_tx_hash, - "state_root" => batch.state_root - } - end - - @doc """ - Function to render GET requests to `/api/v2/zkevm/batches` endpoint. - """ - def render("zkevm_batches.json", %{ - batches: batches, - next_page_params: next_page_params - }) do - %{ - items: render_zkevm_batches(batches), - next_page_params: next_page_params - } - end - - @doc """ - Function to render GET requests to `/api/v2/main-page/zkevm/batches/confirmed` endpoint. - """ - def render("zkevm_batches.json", %{batches: batches}) do - %{items: render_zkevm_batches(batches)} - end - - @doc """ - Function to render GET requests to `/api/v2/zkevm/batches/count` endpoint. - """ - def render("zkevm_batches_count.json", %{count: count}) do - count - end - - @doc """ - Function to render GET requests to `/api/v2/main-page/zkevm/batches/latest-number` endpoint. - """ - def render("zkevm_batch_latest_number.json", %{number: number}) do - number - end - - defp batch_status(batch) do - sequence_id = Map.get(batch, :sequence_id) - verify_id = Map.get(batch, :verify_id) - - cond do - is_nil(sequence_id) && is_nil(verify_id) -> "Unfinalized" - !is_nil(sequence_id) && is_nil(verify_id) -> "L1 Sequence Confirmed" - !is_nil(verify_id) -> "Finalized" - end - end - - defp render_zkevm_batches(batches) do - Enum.map(batches, fn batch -> - sequence_tx_hash = - if not is_nil(batch.sequence_transaction) do - batch.sequence_transaction.hash - end - - verify_tx_hash = - if not is_nil(batch.verify_transaction) do - batch.verify_transaction.hash - end - - %{ - "number" => batch.number, - "status" => batch_status(batch), - "timestamp" => batch.timestamp, - "tx_count" => batch.l2_transactions_count, - "sequence_tx_hash" => sequence_tx_hash, - "verify_tx_hash" => verify_tx_hash - } - end) - end -end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex new file mode 100644 index 000000000000..7f6bf7a26138 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex @@ -0,0 +1,235 @@ +defmodule BlockScoutWeb.API.V2.ZkSyncView do + use BlockScoutWeb, :view + + alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.ZkSync.TransactionBatch + + @doc """ + Function to render GET requests to `/api/v2/zksync/batches/:batch_number` endpoint. + """ + @spec render(binary(), map()) :: map() | non_neg_integer() + def render("zksync_batch.json", %{batch: batch}) do + l2_transactions = + if Map.has_key?(batch, :l2_transactions) do + Enum.map(batch.l2_transactions, fn tx -> tx.hash end) + end + + %{ + "number" => batch.number, + "timestamp" => batch.timestamp, + "root_hash" => batch.root_hash, + "l1_tx_count" => batch.l1_tx_count, + "l2_tx_count" => batch.l2_tx_count, + "l1_gas_price" => batch.l1_gas_price, + "l2_fair_gas_price" => batch.l2_fair_gas_price, + "start_block" => batch.start_block, + "end_block" => batch.end_block, + "transactions" => l2_transactions + } + |> add_l1_txs_info_and_status(batch) + end + + @doc """ + Function to render GET requests to `/api/v2/zksync/batches` endpoint. + """ + def render("zksync_batches.json", %{ + batches: batches, + next_page_params: next_page_params + }) do + %{ + items: render_zksync_batches(batches), + next_page_params: next_page_params + } + end + + @doc """ + Function to render GET requests to `/api/v2/main-page/zksync/batches/confirmed` endpoint. + """ + def render("zksync_batches.json", %{batches: batches}) do + %{items: render_zksync_batches(batches)} + end + + @doc """ + Function to render GET requests to `/api/v2/zksync/batches/count` endpoint. + """ + def render("zksync_batches_count.json", %{count: count}) do + count + end + + @doc """ + Function to render GET requests to `/api/v2/main-page/zksync/batches/latest-number` endpoint. + """ + def render("zksync_batch_latest_number.json", %{number: number}) do + number + end + + defp render_zksync_batches(batches) do + Enum.map(batches, fn batch -> + %{ + "number" => batch.number, + "timestamp" => batch.timestamp, + "tx_count" => batch.l1_tx_count + batch.l2_tx_count + } + |> add_l1_txs_info_and_status(batch) + end) + end + + @doc """ + Extends the json output with a sub-map containing information related + zksync: batch number and associated L1 transactions and their timestmaps. + + ## Parameters + - `out_json`: a map defining output json which will be extended + - `transaction`: transaction structure containing zksync related data + + ## Returns + A map extended with data related zksync rollup + """ + @spec extend_transaction_json_response(map(), %{ + :__struct__ => Explorer.Chain.Transaction, + :zksync_batch => any(), + :zksync_commit_transaction => any(), + :zksync_execute_transaction => any(), + :zksync_prove_transaction => any(), + optional(any()) => any() + }) :: map() + def extend_transaction_json_response(out_json, %Transaction{} = transaction) do + do_add_zksync_info(out_json, transaction) + end + + @doc """ + Extends the json output with a sub-map containing information related + zksync: batch number and associated L1 transactions and their timestmaps. + + ## Parameters + - `out_json`: a map defining output json which will be extended + - `block`: block structure containing zksync related data + + ## Returns + A map extended with data related zksync rollup + """ + @spec extend_block_json_response(map(), %{ + :__struct__ => Explorer.Chain.Block, + :zksync_batch => any(), + :zksync_commit_transaction => any(), + :zksync_execute_transaction => any(), + :zksync_prove_transaction => any(), + optional(any()) => any() + }) :: map() + def extend_block_json_response(out_json, %Block{} = block) do + do_add_zksync_info(out_json, block) + end + + defp do_add_zksync_info(out_json, zksync_entity) do + res = + %{} + |> do_add_l1_txs_info_and_status(%{ + batch_number: get_batch_number(zksync_entity), + commit_transaction: zksync_entity.zksync_commit_transaction, + prove_transaction: zksync_entity.zksync_prove_transaction, + execute_transaction: zksync_entity.zksync_execute_transaction + }) + |> Map.put("batch_number", get_batch_number(zksync_entity)) + + Map.put(out_json, "zksync", res) + end + + defp get_batch_number(zksync_entity) do + case Map.get(zksync_entity, :zksync_batch) do + nil -> nil + %Ecto.Association.NotLoaded{} -> nil + value -> value.number + end + end + + defp add_l1_txs_info_and_status(out_json, %TransactionBatch{} = batch) do + do_add_l1_txs_info_and_status(out_json, batch) + end + + defp do_add_l1_txs_info_and_status(out_json, zksync_item) do + l1_txs = get_associated_l1_txs(zksync_item) + + out_json + |> Map.merge(%{ + "status" => batch_status(zksync_item), + "commit_transaction_hash" => get_2map_data(l1_txs, :commit_transaction, :hash), + "commit_transaction_timestamp" => get_2map_data(l1_txs, :commit_transaction, :ts), + "prove_transaction_hash" => get_2map_data(l1_txs, :prove_transaction, :hash), + "prove_transaction_timestamp" => get_2map_data(l1_txs, :prove_transaction, :ts), + "execute_transaction_hash" => get_2map_data(l1_txs, :execute_transaction, :hash), + "execute_transaction_timestamp" => get_2map_data(l1_txs, :execute_transaction, :ts) + }) + end + + # Extract transaction hash and timestamp for L1 transactions associated with + # a zksync rollup entity: batch, transaction or block. + # + # ## Parameters + # - `zksync_item`: A batch, transaction, or block. + # + # ## Returns + # A map containing nesting maps describing corresponding L1 transactions + defp get_associated_l1_txs(zksync_item) do + [:commit_transaction, :prove_transaction, :execute_transaction] + |> Enum.reduce(%{}, fn key, l1_txs -> + case Map.get(zksync_item, key) do + nil -> Map.put(l1_txs, key, nil) + %Ecto.Association.NotLoaded{} -> Map.put(l1_txs, key, nil) + value -> Map.put(l1_txs, key, %{hash: value.hash, ts: value.timestamp}) + end + end) + end + + # Inspects L1 transactions of the batch to determine the batch status. + # + # ## Parameters + # - `zksync_item`: A batch, transaction, or block. + # + # ## Returns + # A string with one of predefined statuses + defp batch_status(zksync_item) do + cond do + specified?(zksync_item.execute_transaction) -> "Executed on L1" + specified?(zksync_item.prove_transaction) -> "Validated on L1" + specified?(zksync_item.commit_transaction) -> "Sent to L1" + # Batch entity itself has no batch_number + not Map.has_key?(zksync_item, :batch_number) -> "Sealed on L2" + not is_nil(zksync_item.batch_number) -> "Sealed on L2" + true -> "Processed on L2" + end + end + + # Checks if an item associated with a DB entity has actual value + # + # ## Parameters + # - `associated_item`: an item associated with a DB entity + # + # ## Returns + # - `false`: if the item is nil or not loaded + # - `true`: if the item has actual value + defp specified?(associated_item) do + case associated_item do + nil -> false + %Ecto.Association.NotLoaded{} -> false + _ -> true + end + end + + # Gets the value of an element nested in a map using two keys. + # + # Clarification: Returns `map[key1][key2]` + # + # ## Parameters + # - `map`: The high-level map. + # - `key1`: The key of the element in `map`. + # - `key2`: The key of the element in the map accessible by `map[key1]`. + # + # ## Returns + # The value of the element, or `nil` if the map accessible by `key1` does not exist. + defp get_2map_data(map, key1, key2) do + case Map.get(map, key1) do + nil -> nil + inner_map -> Map.get(inner_map, key2) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex index e6355faf1945..2f215b478ced 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.BlockView do alias Explorer.Chain alias Explorer.Chain.{Block, Wei} alias Explorer.Chain.Block.Reward - alias Explorer.Counters.{BlockBurnedFeeCounter, BlockPriorityFeeCounter} + alias Explorer.Counters.{BlockBurntFeeCounter, BlockPriorityFeeCounter} @dialyzer :no_match diff --git a/apps/block_scout_web/lib/block_scout_web/views/bridged_tokens_view.ex b/apps/block_scout_web/lib/block_scout_web/views/bridged_tokens_view.ex new file mode 100644 index 000000000000..3cf7e32512c7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/bridged_tokens_view.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.BridgedTokensView do + use BlockScoutWeb, :view + + alias Explorer.Chain.{BridgedToken, CurrencyHelper, Token} + + @doc """ + Calculates capitalization of the bridged token in USD. + """ + @spec bridged_token_usd_cap(BridgedToken.t(), Token.t()) :: String.t() + def bridged_token_usd_cap(bridged_token, token) do + usd_cap = + if bridged_token.custom_cap do + bridged_token.custom_cap + else + if bridged_token.exchange_rate && token.total_supply do + Decimal.mult(bridged_token.exchange_rate, CurrencyHelper.divide_decimals(token.total_supply, token.decimals)) + else + Decimal.new(0) + end + end + + usd_cap |> Decimal.to_float() |> :erlang.float_to_binary([:compact, decimals: 20]) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex b/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex index 43d66a29266d..bbbcdebe866e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex @@ -60,10 +60,7 @@ defmodule BlockScoutWeb.ChainView do defp gas_prices do case GasPriceOracle.get_gas_prices() do {:ok, gas_prices} -> - gas_prices - - nil -> - nil + %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]} _ -> nil diff --git a/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex b/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex index fa3ad1f1c1f8..0d1c0e1e139f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/cldr_helper/number.ex @@ -17,6 +17,8 @@ defmodule BlockScoutWeb.CldrHelper.Number do end end + def to_string!(nil), do: "" + def to_string!(decimal) do # We do this to trick Dialyzer to not complain about non-local returns caused by bug in Cldr.Number.to_string! spec case :erlang.phash2(1, 1) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex index 9cff33b73249..46977c4e1f51 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.LayoutView do use BlockScoutWeb, :view alias EthereumJSONRPC.Variant - alias Explorer.Chain + alias Explorer.{Chain, Helper} alias Poison.Parser import BlockScoutWeb.APIDocsView, only: [blockscout_url: 1] @@ -203,7 +203,7 @@ defmodule BlockScoutWeb.LayoutView do def webapp_url(conn) do :block_scout_web |> Application.get_env(:webapp_url) - |> validate_url() + |> Helper.validate_url() |> case do :error -> chain_path(conn, :show) {:ok, url} -> url @@ -213,7 +213,7 @@ defmodule BlockScoutWeb.LayoutView do def api_url do :block_scout_web |> Application.get_env(:api_url) - |> validate_url() + |> Helper.validate_url() |> case do :error -> "" {:ok, url} -> url @@ -236,15 +236,6 @@ defmodule BlockScoutWeb.LayoutView do end end - defp validate_url(url) when is_binary(url) do - case URI.parse(url) do - %URI{host: nil} -> :error - _ -> {:ok, url} - end - end - - defp validate_url(_), do: :error - def sign_in_link do if Mix.env() == :test do "/auth/auth0" diff --git a/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex b/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex index 20e4cca0596f..9939ec3e684f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/robots_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.RobotsView do alias BlockScoutWeb.APIDocsView alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Token} @limit 200 defp limit, do: @limit diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index f0245adc2dfc..ad32647d14e0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -6,6 +6,8 @@ defmodule BlockScoutWeb.SmartContractView do alias Explorer.Chain alias Explorer.Chain.{Address, Transaction} alias Explorer.Chain.Hash.Address, as: HashAddress + alias Explorer.Chain.SmartContract + alias Explorer.Chain.SmartContract.Proxy.EIP1167 alias Explorer.SmartContract.Helper require Logger @@ -210,7 +212,7 @@ defmodule BlockScoutWeb.SmartContractView do end def decode_revert_reason(to_address, revert_reason, options \\ []) do - smart_contract = Chain.address_hash_to_smart_contract(to_address, options) + smart_contract = SmartContract.address_hash_to_smart_contract(to_address, options) Transaction.decoded_revert_reason( %Transaction{to_address: %{smart_contract: smart_contract}, hash: to_address}, diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex index 51119e8d7a98..d7103f0314d4 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex @@ -16,31 +16,70 @@ defmodule BlockScoutWeb.Tokens.Helper do When the token's type is ERC-721, the function will return a string with the token_id that represents the ERC-721 token since this kind of token doesn't have amount and decimals. """ - def token_transfer_amount(%{token: token, amount: amount, amounts: amounts, token_ids: token_ids}) do - do_token_transfer_amount(token, amount, amounts, token_ids) + def token_transfer_amount(%{ + token: token, + token_type: token_type, + amount: amount, + amounts: amounts, + token_ids: token_ids + }) do + do_token_transfer_amount(token, token_type, amount, amounts, token_ids) end - def token_transfer_amount(%{token: token, amount: amount, token_ids: token_ids}) do - do_token_transfer_amount(token, amount, nil, token_ids) + def token_transfer_amount(%{token: token, token_type: token_type, amount: amount, token_ids: token_ids}) do + do_token_transfer_amount(token, token_type, amount, nil, token_ids) end - defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, nil, _token_ids) do + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: type}, nil, nil, nil, _token_ids) when type in ["ERC-20", "ERC-404"] do {:ok, "--"} end - defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, amount, _amounts, _token_ids) do + defp do_token_transfer_amount(_token, type, nil, nil, _token_ids) when type in ["ERC-20", "ERC-404"] do + {:ok, "--"} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: type, decimals: nil}, nil, amount, _amounts, _token_ids) + when type in ["ERC-20", "ERC-404"] do + {:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))} + end + + defp do_token_transfer_amount(%Token{decimals: nil}, type, amount, _amounts, _token_ids) + when type in ["ERC-20", "ERC-404"] do {:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))} end - defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, amount, _amounts, _token_ids) do + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: type, decimals: decimals}, nil, amount, _amounts, _token_ids) + when type in ["ERC-20", "ERC-404"] do {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} end - defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, _amounts, _token_ids) do + defp do_token_transfer_amount(%Token{decimals: decimals}, type, amount, _amounts, _token_ids) + when type in ["ERC-20", "ERC-404"] do + {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ERC-721"}, nil, _amount, _amounts, _token_ids) do + {:ok, :erc721_instance} + end + + defp do_token_transfer_amount(_token, "ERC-721", _amount, _amounts, _token_ids) do {:ok, :erc721_instance} end - defp do_token_transfer_amount(%Token{type: "ERC-1155", decimals: decimals}, amount, amounts, token_ids) do + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ERC-1155", decimals: decimals}, nil, amount, amounts, token_ids) do + if amount do + {:ok, :erc1155_instance, CurrencyHelper.format_according_to_decimals(amount, decimals)} + else + {:ok, :erc1155_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount(%Token{decimals: decimals}, "ERC-1155", amount, amounts, token_ids) do if amount do {:ok, :erc1155_instance, CurrencyHelper.format_according_to_decimals(amount, decimals)} else @@ -48,42 +87,84 @@ defmodule BlockScoutWeb.Tokens.Helper do end end - defp do_token_transfer_amount(_token, _amount, _amounts, _token_ids) do + defp do_token_transfer_amount(_token, _token_type, _amount, _amounts, _token_ids) do nil end def token_transfer_amount_for_api(%{ token: token, + token_type: token_type, amount: amount, amounts: amounts, token_ids: token_ids }) do - do_token_transfer_amount_for_api(token, amount, amounts, token_ids) + do_token_transfer_amount_for_api(token, token_type, amount, amounts, token_ids) + end + + def token_transfer_amount_for_api(%{token: token, token_type: token_type, amount: amount, token_ids: token_ids}) do + do_token_transfer_amount_for_api(token, token_type, amount, nil, token_ids) end - def token_transfer_amount_for_api(%{token: token, amount: amount, token_ids: token_ids}) do - do_token_transfer_amount_for_api(token, amount, nil, token_ids) + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api(%Token{type: type}, nil, nil, nil, _token_ids) + when type in ["ERC-20", "ERC-404"] do + {:ok, nil} end - defp do_token_transfer_amount_for_api(%Token{type: "ERC-20"}, nil, nil, _token_ids) do + defp do_token_transfer_amount_for_api(_token, type, nil, nil, _token_ids) when type in ["ERC-20", "ERC-404"] do {:ok, nil} end + # TODO: remove this clause along with token transfer denormalization defp do_token_transfer_amount_for_api( - %Token{type: "ERC-20", decimals: decimals}, + %Token{type: type, decimals: decimals}, + nil, amount, _amounts, _token_ids - ) do + ) + when type in ["ERC-20", "ERC-404"] do {:ok, amount, decimals} end - defp do_token_transfer_amount_for_api(%Token{type: "ERC-721"}, _amount, _amounts, _token_ids) do + defp do_token_transfer_amount_for_api( + %Token{decimals: decimals}, + type, + amount, + _amounts, + _token_ids + ) + when type in ["ERC-20", "ERC-404"] do + {:ok, amount, decimals} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api(%Token{type: "ERC-721"}, nil, _amount, _amounts, _token_ids) do + {:ok, :erc721_instance} + end + + defp do_token_transfer_amount_for_api(_token, "ERC-721", _amount, _amounts, _token_ids) do {:ok, :erc721_instance} end + # TODO: remove this clause along with token transfer denormalization defp do_token_transfer_amount_for_api( %Token{type: "ERC-1155", decimals: decimals}, + nil, + amount, + amounts, + token_ids + ) do + if amount do + {:ok, :erc1155_instance, amount, decimals} + else + {:ok, :erc1155_instance, amounts, token_ids, decimals} + end + end + + defp do_token_transfer_amount_for_api( + %Token{decimals: decimals}, + "ERC-1155", amount, amounts, token_ids @@ -95,7 +176,7 @@ defmodule BlockScoutWeb.Tokens.Helper do end end - defp do_token_transfer_amount_for_api(_token, _amount, _amounts, _token_ids) do + defp do_token_transfer_amount_for_api(_token, _token_type, _amount, _amounts, _token_ids) do nil end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex index 2edfd9398119..745b041ddd64 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex @@ -70,6 +70,16 @@ defmodule BlockScoutWeb.Tokens.HolderView do to_string(format_according_to_decimals(value, decimals)) <> " TokenID " <> to_string(id) end + def format_token_balance_value(value, id, %Token{type: "ERC-404", decimals: decimals}) do + base = to_string(format_according_to_decimals(value, decimals)) + + if id do + base <> " TokenID " <> to_string(id) + else + base + end + end + def format_token_balance_value(value, _id, _token) do value end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex index 9bc2dbfe45e6..497186464c0c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do alias Explorer.{Chain, CustomContractsHelper} alias Explorer.Chain.{Address, SmartContract, Token} + alias Explorer.Chain.SmartContract.Proxy alias Explorer.SmartContract.{Helper, Writer} alias BlockScoutWeb.{AccessHelper, CurrencyHelper, LayoutView} @@ -43,6 +44,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do def display_inventory?(%Token{type: "ERC-721"}), do: true def display_inventory?(%Token{type: "ERC-1155"}), do: true + def display_inventory?(%Token{type: "ERC-404"}), do: true def display_inventory?(_), do: false def smart_contract_with_read_only_functions?( @@ -53,11 +55,13 @@ defmodule BlockScoutWeb.Tokens.OverviewView do def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false - def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: %SmartContract{} = smart_contract}}) do - SmartContract.proxy_contract?(smart_contract) + def token_smart_contract_is_proxy?(%Token{ + contract_address: %Address{smart_contract: %SmartContract{} = smart_contract} + }) do + Proxy.proxy_contract?(smart_contract) end - def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: nil}}), do: false + def token_smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: nil}}), do: false def smart_contract_with_write_functions?(%Token{ contract_address: %Address{smart_contract: %SmartContract{}} = address diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 548903cdd583..587c57cf062f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -32,11 +32,13 @@ defmodule BlockScoutWeb.TransactionView do defdelegate formatted_timestamp(block), to: BlockView def block_number(%Transaction{block_number: nil}), do: gettext("Block Pending") - def block_number(%Transaction{block: block}), do: [view_module: BlockView, partial: "_link.html", block: block] + + def block_number(%Transaction{block_number: number, block_hash: hash}), + do: [view_module: BlockView, partial: "_link.html", block: %Block{number: number, hash: hash}] + def block_number(%Reward{block: block}), do: [view_module: BlockView, partial: "_link.html", block: block] - def block_timestamp(%Transaction{block_number: nil, inserted_at: time}), do: time - def block_timestamp(%Transaction{block: %Block{timestamp: time}}), do: time + def block_timestamp(%Transaction{} = transaction), do: Transaction.block_timestamp(transaction) def block_timestamp(%Reward{block: %Block{timestamp: time}}), do: time def value_transfer?(%Transaction{input: %{bytes: bytes}}) when bytes in [<<>>, nil] do @@ -146,6 +148,7 @@ defmodule BlockScoutWeb.TransactionView do amount: nil, amounts: [], token_ids: token_transfer.token_ids, + token_type: token_transfer.token_type, to_address_hash: token_transfer.to_address_hash, from_address_hash: token_transfer.from_address_hash } @@ -160,6 +163,7 @@ defmodule BlockScoutWeb.TransactionView do amount: nil, amounts: amounts, token_ids: token_transfer.token_ids, + token_type: token_transfer.token_type, to_address_hash: token_transfer.to_address_hash, from_address_hash: token_transfer.from_address_hash } @@ -173,6 +177,7 @@ defmodule BlockScoutWeb.TransactionView do amount: token_transfer.amount, amounts: [], token_ids: token_transfer.token_ids, + token_type: token_transfer.token_type, to_address_hash: token_transfer.to_address_hash, from_address_hash: token_transfer.from_address_hash } @@ -218,6 +223,7 @@ defmodule BlockScoutWeb.TransactionView do :erc20 -> gettext("ERC-20 ") :erc721 -> gettext("ERC-721 ") :erc1155 -> gettext("ERC-1155 ") + :erc404 -> gettext("ERC-404 ") _ -> "" end end @@ -308,7 +314,7 @@ defmodule BlockScoutWeb.TransactionView do def contract_creation?(_), do: false def fee(%Transaction{} = transaction) do - {_, value} = Chain.fee(transaction, :wei) + {_, value} = Transaction.fee(transaction, :wei) value end @@ -318,7 +324,7 @@ defmodule BlockScoutWeb.TransactionView do def formatted_fee(%Transaction{} = transaction, opts) do transaction - |> Chain.fee(:wei) + |> Transaction.fee(:wei) |> fee_to_denomination(opts) |> case do {:actual, value} -> value @@ -404,12 +410,26 @@ defmodule BlockScoutWeb.TransactionView do format_wei_value(gas_price, unit) end + def l1_gas_price(transaction, unit) when unit in ~w(wei gwei ether)a do + case Map.get(transaction, :l1_gas_price) do + nil -> nil + value -> format_wei_value(value, unit) + end + end + def gas_used(%Transaction{gas_used: nil}), do: gettext("Pending") def gas_used(%Transaction{gas_used: gas_used}) do Number.to_string!(gas_used) end + def l1_gas_used(transaction) do + case Map.get(transaction, :l1_gas_used) do + nil -> gettext("Pending") + value -> Number.to_string!(value) + end + end + def gas_used_perc(%Transaction{gas_used: nil}), do: nil def gas_used_perc(%Transaction{gas_used: gas_used, gas: gas}) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex index 3b507d46557c..72c1c1ee1f3a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/wei_helper.ex @@ -52,8 +52,12 @@ defmodule BlockScoutWeb.WeiHelper do ...> ) "10" """ - @spec format_wei_value(Wei.t(), Wei.unit(), format_options()) :: String.t() - def format_wei_value(%Wei{} = wei, unit, options \\ []) when unit in @valid_units do + @spec format_wei_value(Wei.t() | nil, Wei.unit(), format_options()) :: String.t() | nil + def format_wei_value(_wei, _unit, _options \\ []) + + def format_wei_value(nil, _unit, _options), do: nil + + def format_wei_value(%Wei{} = wei, unit, options) when unit in @valid_units do converted_value = wei |> Wei.to(unit) diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 89a4cdcffb66..958d21855781 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.Mixfile do deps_path: "../../deps", description: "Web interface for BlockScout.", dialyzer: [ - plt_add_deps: :transitive, + plt_add_deps: :app_tree, ignore_warnings: "../../.dialyzer-ignore" ], elixir: "~> 1.13", @@ -23,8 +23,17 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "5.3.1", - xref: [exclude: [Explorer.Chain.Zkevm.Reader]] + version: "6.3.0", + xref: [ + exclude: [ + Explorer.Chain.PolygonZkevm.Reader, + Explorer.Chain.Beacon.Reader, + Explorer.Chain.Cache.OptimismFinalizationPeriod, + Explorer.Chain.Optimism.OutputRoot, + Explorer.Chain.Optimism.WithdrawalEvent, + Explorer.Chain.ZkSync.Reader + ] + ] ] end @@ -84,7 +93,7 @@ defmodule BlockScoutWeb.Mixfile do # HTML CSS selectors for Phoenix controller tests {:floki, "~> 0.31"}, {:flow, "~> 1.2"}, - {:gettext, "~> 0.23.1"}, + {:gettext, "~> 0.24.0"}, {:hammer, "~> 6.0"}, {:httpoison, "~> 2.0"}, {:indexer, in_umbrella: true, runtime: false}, @@ -132,7 +141,8 @@ defmodule BlockScoutWeb.Mixfile do {:ex_json_schema, "~> 0.10.1"}, {:ueberauth, "~> 0.7"}, {:ueberauth_auth0, "~> 2.0"}, - {:bureaucrat, "~> 0.2.9", only: :test} + {:bureaucrat, "~> 0.2.9", only: :test}, + {:logger_json, "~> 5.1"} ] end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index d15c1455abf1..d56fadb7cfb6 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -1,16 +1,15 @@ -#: lib/block_scout_web/views/address_token_balance_view.ex:10 -#, elixir-autogen, elixir-format -msgid "%{count} token" -msgid_plural "%{count} tokens" -msgstr[0] "" -msgstr[1] "" - -#: lib/block_scout_web/templates/block/_tile.html.eex:29 -#, elixir-autogen, elixir-format -msgid "%{count} transaction" -msgid_plural "%{count} transactions" -msgstr[0] "" -msgstr[1] "" +## This file is a PO Template file. +## +## "msgid"s here are often extracted from source code. +## Add new messages manually only if they're dynamic +## messages that can't be statically extracted. +## +## Run "mix gettext.extract" to bring this file up to +## date. Leave "msgstr"s empty as changing them here has no +## effect: edit them in PO (.po) files instead. +# +msgid "" +msgstr "" #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 #, elixir-autogen, elixir-format @@ -53,6 +52,20 @@ msgstr "" msgid "%{count} Transactions" msgstr "" +#: lib/block_scout_web/views/address_token_balance_view.ex:10 +#, elixir-autogen, elixir-format +msgid "%{count} token" +msgid_plural "%{count} tokens" +msgstr[0] "" +msgstr[1] "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:29 +#, elixir-autogen, elixir-format +msgid "%{count} transaction" +msgid_plural "%{count} transactions" +msgstr[0] "" +msgstr[1] "" + #: lib/block_scout_web/templates/transaction/_actions.html.eex:101 #, elixir-autogen, elixir-format msgid "%{qty} of Token ID [%{link_to_id}]" @@ -68,7 +81,7 @@ msgstr "" msgid "%{subnetwork} Explorer - BlockScout" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:369 +#: lib/block_scout_web/views/transaction_view.ex:375 #, elixir-autogen, elixir-format msgid "(Awaiting internal transactions for status)" msgstr "" @@ -86,7 +99,7 @@ msgstr "" msgid ") may be added for each contract. Click the Add Library button to add an additional one." msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:90 +#: lib/block_scout_web/templates/layout/app.html.eex:93 #, elixir-autogen, elixir-format msgid "- We're indexing this chain right now. Some of the counts may be inaccurate." msgstr "" @@ -122,7 +135,7 @@ msgstr "" msgid "A block producer who successfully included the block onto the blockchain." msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "A confirmation email was sent to" msgstr "" @@ -199,11 +212,6 @@ msgstr "" msgid "Actions" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:451 -#, elixir-autogen, elixir-format -msgid "Actual gas amount used by the transaction." -msgstr "" - #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 @@ -265,17 +273,17 @@ msgstr "" #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20 #: lib/block_scout_web/templates/transaction_state/index.html.eex:34 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 -#: lib/block_scout_web/views/address_view.ex:108 +#: lib/block_scout_web/views/address_view.ex:109 #, elixir-autogen, elixir-format msgid "Address" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:243 +#: lib/block_scout_web/templates/transaction/overview.html.eex:263 #, elixir-autogen, elixir-format msgid "Address (external or contract) receiving the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:225 +#: lib/block_scout_web/templates/transaction/overview.html.eex:245 #, elixir-autogen, elixir-format msgid "Address (external or contract) sending the transaction." msgstr "" @@ -336,7 +344,7 @@ msgstr "" msgid "All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:28 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "All metadata displayed below is from that contract. In order to verify current contract, click" msgstr "" @@ -346,14 +354,7 @@ msgstr "" msgid "All tokens in the account and total value." msgstr "" -#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 -#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 -#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 -#, elixir-autogen, elixir-format -msgid "Amount" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:437 +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 #, elixir-autogen, elixir-format msgid "Amount of" msgstr "" @@ -383,11 +384,6 @@ msgstr "" msgid "Average" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:101 -#, elixir-autogen, elixir-format -msgid "Average block time" -msgstr "" - #: lib/block_scout_web/templates/account/api_key/form.html.eex:25 #, elixir-autogen, elixir-format msgid "Back to API keys (Cancel)" @@ -455,17 +451,7 @@ msgstr "" msgid "Base URL:" msgstr "" -#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 -#, elixir-autogen, elixir-format -msgid "Beacon chain withdrawals - %{subnetwork} Explorer" -msgstr "" - -#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:472 +#: lib/block_scout_web/templates/transaction/overview.html.eex:545 #, elixir-autogen, elixir-format msgid "Binary data included with the transaction. See input / logs below for additional info." msgstr "" @@ -541,7 +527,7 @@ msgstr "" msgid "Blockchain" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:154 +#: lib/block_scout_web/templates/chain/show.html.eex:156 #: lib/block_scout_web/templates/layout/_topnav.html.eex:34 #: lib/block_scout_web/templates/layout/_topnav.html.eex:38 #, elixir-autogen, elixir-format @@ -556,7 +542,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/overview.html.eex:275 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:385 +#: lib/block_scout_web/views/address_view.ex:386 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" @@ -656,13 +642,13 @@ msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:378 +#: lib/block_scout_web/views/address_view.ex:379 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 -#: lib/block_scout_web/views/address_view.ex:384 +#: lib/block_scout_web/views/address_view.ex:385 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" @@ -688,17 +674,17 @@ msgstr "" msgid "Compiler" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:143 +#: lib/block_scout_web/templates/address_contract/index.html.eex:142 #, elixir-autogen, elixir-format msgid "Compiler Settings" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:72 +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 #, elixir-autogen, elixir-format msgid "Compiler version" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:362 +#: lib/block_scout_web/views/transaction_view.ex:368 #, elixir-autogen, elixir-format msgid "Confirmed" msgstr "" @@ -747,7 +733,7 @@ msgstr "" msgid "Connection Lost, click to load newer validations" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:97 +#: lib/block_scout_web/templates/address_contract/index.html.eex:96 #, elixir-autogen, elixir-format msgid "Constructor Arguments" msgstr "" @@ -758,12 +744,12 @@ msgid "Constructor args" msgstr "" #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 -#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 #, elixir-autogen, elixir-format msgid "Contract" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:158 +#: lib/block_scout_web/templates/address_contract/index.html.eex:157 #, elixir-autogen, elixir-format msgid "Contract ABI" msgstr "" @@ -771,30 +757,30 @@ msgstr "" #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 -#: lib/block_scout_web/views/address_view.ex:106 +#: lib/block_scout_web/views/address_view.ex:107 #, elixir-autogen, elixir-format msgid "Contract Address" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 -#: lib/block_scout_web/views/address_view.ex:46 -#: lib/block_scout_web/views/address_view.ex:80 +#: lib/block_scout_web/views/address_view.ex:47 +#: lib/block_scout_web/views/address_view.ex:81 #, elixir-autogen, elixir-format msgid "Contract Address Pending" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:478 +#: lib/block_scout_web/views/transaction_view.ex:497 #, elixir-autogen, elixir-format msgid "Contract Call" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:475 +#: lib/block_scout_web/views/transaction_view.ex:494 #, elixir-autogen, elixir-format msgid "Contract Creation" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:175 -#: lib/block_scout_web/templates/address_contract/index.html.eex:190 +#: lib/block_scout_web/templates/address_contract/index.html.eex:174 +#: lib/block_scout_web/templates/address_contract/index.html.eex:189 #, elixir-autogen, elixir-format msgid "Contract Creation Code" msgstr "" @@ -811,7 +797,7 @@ msgstr "" msgid "Contract Name" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:26 +#: lib/block_scout_web/templates/address_contract/index.html.eex:25 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 #, elixir-autogen, elixir-format msgid "Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB" @@ -822,12 +808,12 @@ msgstr "" msgid "Contract name or address" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:64 +#: lib/block_scout_web/templates/address_contract/index.html.eex:63 #, elixir-autogen, elixir-format msgid "Contract name:" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:107 +#: lib/block_scout_web/templates/address_contract/index.html.eex:106 #, elixir-autogen, elixir-format msgid "Contract source code" msgstr "" @@ -842,7 +828,7 @@ msgstr "" msgid "Contracts" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:181 +#: lib/block_scout_web/templates/address_contract/index.html.eex:180 #, elixir-autogen, elixir-format msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." msgstr "" @@ -852,7 +838,7 @@ msgstr "" msgid "Contribute" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:160 +#: lib/block_scout_web/templates/address_contract/index.html.eex:159 #, elixir-autogen, elixir-format msgid "Copy ABI" msgstr "" @@ -878,7 +864,7 @@ msgstr "" msgid "Copy Address" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:145 +#: lib/block_scout_web/templates/address_contract/index.html.eex:144 #, elixir-autogen, elixir-format msgid "Copy Compiler Settings" msgstr "" @@ -889,8 +875,8 @@ msgstr "" msgid "Copy Contract Address" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:177 -#: lib/block_scout_web/templates/address_contract/index.html.eex:193 +#: lib/block_scout_web/templates/address_contract/index.html.eex:176 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 #, elixir-autogen, elixir-format msgid "Copy Contract Creation Code" msgstr "" @@ -900,8 +886,8 @@ msgstr "" msgid "Copy Decompiled Contract Code" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:214 -#: lib/block_scout_web/templates/address_contract/index.html.eex:224 +#: lib/block_scout_web/templates/address_contract/index.html.eex:213 +#: lib/block_scout_web/templates/address_contract/index.html.eex:223 #, elixir-autogen, elixir-format msgid "Copy Deployed ByteCode" msgstr "" @@ -909,8 +895,8 @@ msgstr "" #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 -#: lib/block_scout_web/templates/transaction/overview.html.eex:233 -#: lib/block_scout_web/templates/transaction/overview.html.eex:234 +#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:254 #, elixir-autogen, elixir-format msgid "Copy From Address" msgstr "" @@ -937,18 +923,18 @@ msgstr "" msgid "Copy Raw Trace" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:121 -#: lib/block_scout_web/templates/address_contract/index.html.eex:133 +#: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:132 #, elixir-autogen, elixir-format msgid "Copy Source Code" msgstr "" #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 -#: lib/block_scout_web/templates/transaction/overview.html.eex:260 -#: lib/block_scout_web/templates/transaction/overview.html.eex:261 -#: lib/block_scout_web/templates/transaction/overview.html.eex:268 -#: lib/block_scout_web/templates/transaction/overview.html.eex:269 +#: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:281 +#: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 #, elixir-autogen, elixir-format msgid "Copy To Address" msgstr "" @@ -969,20 +955,20 @@ msgstr "" msgid "Copy Txn Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:498 +#: lib/block_scout_web/templates/transaction/overview.html.eex:571 #, elixir-autogen, elixir-format msgid "Copy Txn Hex Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:504 +#: lib/block_scout_web/templates/transaction/overview.html.eex:577 #, elixir-autogen, elixir-format msgid "Copy Txn UTF-8 Input" msgstr "" #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 -#: lib/block_scout_web/templates/transaction/overview.html.eex:497 -#: lib/block_scout_web/templates/transaction/overview.html.eex:503 +#: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:576 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 #, elixir-autogen, elixir-format msgid "Copy Value" @@ -1049,7 +1035,7 @@ msgstr "" msgid "Daily Transactions" msgstr "" -#: lib/block_scout_web/templates/address_logs/_logs.html.eex:101 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:121 @@ -1084,7 +1070,7 @@ msgstr "" msgid "Decoded" msgstr "" -#: lib/block_scout_web/views/address_view.ex:379 +#: lib/block_scout_web/views/address_view.ex:380 #, elixir-autogen, elixir-format msgid "Decompiled Code" msgstr "" @@ -1109,8 +1095,8 @@ msgstr "" msgid "Delegate Call" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:212 -#: lib/block_scout_web/templates/address_contract/index.html.eex:220 +#: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:219 #, elixir-autogen, elixir-format msgid "Deployed ByteCode" msgstr "" @@ -1140,7 +1126,7 @@ msgstr "" msgid "Difficulty" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:182 +#: lib/block_scout_web/templates/address_contract/index.html.eex:181 #, elixir-autogen, elixir-format msgid "Displaying the init data provided of the creating transaction." msgstr "" @@ -1186,12 +1172,12 @@ msgstr "" msgid "EIP-1167" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:220 +#: lib/block_scout_web/views/transaction_view.ex:225 #, elixir-autogen, elixir-format msgid "ERC-1155 " msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:218 +#: lib/block_scout_web/views/transaction_view.ex:223 #, elixir-autogen, elixir-format msgid "ERC-20 " msgstr "" @@ -1201,7 +1187,7 @@ msgstr "" msgid "ERC-20 tokens (beta)" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:219 +#: lib/block_scout_web/views/transaction_view.ex:224 #, elixir-autogen, elixir-format msgid "ERC-721 " msgstr "" @@ -1216,7 +1202,7 @@ msgstr "" msgid "ETH RPC API Documentation" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:83 +#: lib/block_scout_web/templates/address_contract/index.html.eex:82 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 #, elixir-autogen, elixir-format @@ -1292,12 +1278,12 @@ msgstr "" msgid "Error trying to fetch balances." msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:373 +#: lib/block_scout_web/views/transaction_view.ex:379 #, elixir-autogen, elixir-format msgid "Error: %{reason}" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:371 +#: lib/block_scout_web/views/transaction_view.ex:377 #, elixir-autogen, elixir-format msgid "Error: (Awaiting internal transactions for reason)" msgstr "" @@ -1329,12 +1315,17 @@ msgstr "" msgid "Expand" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Export" +msgstr "" + #: lib/block_scout_web/templates/csv_export/index.html.eex:10 #, elixir-autogen, elixir-format msgid "Export Data" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:249 +#: lib/block_scout_web/templates/address_contract/index.html.eex:248 #, elixir-autogen, elixir-format msgid "External libraries" msgstr "" @@ -1411,7 +1402,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 #: lib/block_scout_web/templates/address_transaction/index.html.eex:34 -#: lib/block_scout_web/templates/transaction/overview.html.eex:226 +#: lib/block_scout_web/templates/transaction/overview.html.eex:246 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10 @@ -1426,16 +1417,11 @@ msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/overview.html.eex:187 -#: lib/block_scout_web/templates/transaction/overview.html.eex:399 +#: lib/block_scout_web/templates/transaction/overview.html.eex:430 #, elixir-autogen, elixir-format msgid "Gas Limit" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:379 -#, elixir-autogen, elixir-format -msgid "Gas Price" -msgstr "" - #: lib/block_scout_web/templates/address/overview.html.eex:240 #: lib/block_scout_web/templates/block/_tile.html.eex:73 #: lib/block_scout_web/templates/block/overview.html.eex:178 @@ -1443,11 +1429,6 @@ msgstr "" msgid "Gas Used" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:452 -#, elixir-autogen, elixir-format -msgid "Gas Used by Transaction" -msgstr "" - #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 #, elixir-autogen, elixir-format @@ -1485,7 +1466,7 @@ msgstr "" #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 #: lib/block_scout_web/views/block_view.ex:22 -#: lib/block_scout_web/views/wei_helper.ex:77 +#: lib/block_scout_web/views/wei_helper.ex:81 #, elixir-autogen, elixir-format msgid "Gwei" msgstr "" @@ -1495,13 +1476,13 @@ msgstr "" msgid "Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:480 -#: lib/block_scout_web/templates/transaction/overview.html.eex:484 +#: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:557 #, elixir-autogen, elixir-format msgid "Hex (Default)" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:204 +#: lib/block_scout_web/templates/transaction/overview.html.eex:224 #, elixir-autogen, elixir-format msgid "Highlighted events of the transaction." msgstr "" @@ -1560,14 +1541,7 @@ msgstr "" msgid "Incoming" msgstr "" -#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 -#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 -#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 -#, elixir-autogen, elixir-format -msgid "Index" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:464 +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Index position of Transaction in the block." msgstr "" @@ -1587,7 +1561,7 @@ msgstr "" msgid "Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:245 +#: lib/block_scout_web/templates/transaction/overview.html.eex:265 #, elixir-autogen, elixir-format msgid "Interacted With (To)" msgstr "" @@ -1601,8 +1575,8 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:375 -#: lib/block_scout_web/views/transaction_view.ex:533 +#: lib/block_scout_web/views/address_view.ex:376 +#: lib/block_scout_web/views/transaction_view.ex:552 #, elixir-autogen, elixir-format msgid "Internal Transactions" msgstr "" @@ -1614,7 +1588,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 -#: lib/block_scout_web/views/tokens/overview_view.ex:42 +#: lib/block_scout_web/views/tokens/overview_view.ex:43 #, elixir-autogen, elixir-format msgid "Inventory" msgstr "" @@ -1662,22 +1636,22 @@ msgstr "" msgid "License ID" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:331 +#: lib/block_scout_web/templates/transaction/overview.html.eex:351 #, elixir-autogen, elixir-format msgid "List of ERC-1155 tokens created in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:315 +#: lib/block_scout_web/templates/transaction/overview.html.eex:335 #, elixir-autogen, elixir-format msgid "List of token burnt in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:298 +#: lib/block_scout_web/templates/transaction/overview.html.eex:318 #, elixir-autogen, elixir-format msgid "List of token minted in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:282 +#: lib/block_scout_web/templates/transaction/overview.html.eex:302 #, elixir-autogen, elixir-format msgid "List of token transferred in the transaction." msgstr "" @@ -1718,8 +1692,8 @@ msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:386 -#: lib/block_scout_web/views/transaction_view.ex:534 +#: lib/block_scout_web/views/address_view.ex:387 +#: lib/block_scout_web/views/transaction_view.ex:553 #, elixir-autogen, elixir-format msgid "Logs" msgstr "" @@ -1732,7 +1706,7 @@ msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:53 #: lib/block_scout_web/templates/layout/app.html.eex:50 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 -#: lib/block_scout_web/views/address_view.ex:146 +#: lib/block_scout_web/views/address_view.ex:147 #, elixir-autogen, elixir-format msgid "Market Cap" msgstr "" @@ -1742,27 +1716,22 @@ msgstr "" msgid "Market cap" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:408 +#: lib/block_scout_web/templates/transaction/overview.html.eex:440 #, elixir-autogen, elixir-format msgid "Max Fee per Gas" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:418 +#: lib/block_scout_web/templates/transaction/overview.html.eex:450 #, elixir-autogen, elixir-format msgid "Max Priority Fee per Gas" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:325 +#: lib/block_scout_web/views/transaction_view.ex:331 #, elixir-autogen, elixir-format msgid "Max of" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:398 -#, elixir-autogen, elixir-format -msgid "Maximum gas amount approved for the transaction." -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:407 +#: lib/block_scout_web/templates/transaction/overview.html.eex:439 #, elixir-autogen, elixir-format msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." msgstr "" @@ -1824,7 +1793,7 @@ msgid "More internal transactions have come in" msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:46 -#: lib/block_scout_web/templates/chain/show.html.eex:217 +#: lib/block_scout_web/templates/chain/show.html.eex:219 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 #: lib/block_scout_web/templates/transaction/index.html.eex:19 #, elixir-autogen, elixir-format @@ -1904,6 +1873,16 @@ msgstr "" msgid "New Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via Standard input JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via metadata JSON" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #, elixir-autogen, elixir-format @@ -1944,7 +1923,7 @@ msgid "No trace entries found." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:196 -#: lib/block_scout_web/templates/transaction/overview.html.eex:462 +#: lib/block_scout_web/templates/transaction/overview.html.eex:535 #, elixir-autogen, elixir-format msgid "Nonce" msgstr "" @@ -2002,12 +1981,12 @@ msgstr "" msgid "Optimization" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:68 +#: lib/block_scout_web/templates/address_contract/index.html.eex:67 #, elixir-autogen, elixir-format msgid "Optimization enabled" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:77 +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 #, elixir-autogen, elixir-format @@ -2064,8 +2043,9 @@ msgid "Parent Hash" msgstr "" #: lib/block_scout_web/templates/layout/_topnav.html.eex:63 -#: lib/block_scout_web/views/transaction_view.ex:368 -#: lib/block_scout_web/views/transaction_view.ex:407 +#: lib/block_scout_web/views/transaction_view.ex:373 +#: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:427 #, elixir-autogen, elixir-format msgid "Pending" msgstr "" @@ -2082,7 +2062,7 @@ msgid "Play" msgstr "" #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:22 -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Please confirm your email address to use the My Account feature." msgstr "" @@ -2097,7 +2077,7 @@ msgstr "" msgid "Please select what types of notifications you will receive:" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:464 +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Position" msgstr "" @@ -2134,13 +2114,8 @@ msgstr "" msgid "Price per token on the exchanges" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:378 -#, elixir-autogen, elixir-format -msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." -msgstr "" - #: lib/block_scout_web/templates/block/overview.html.eex:225 -#: lib/block_scout_web/templates/transaction/overview.html.eex:428 +#: lib/block_scout_web/templates/transaction/overview.html.eex:460 #, elixir-autogen, elixir-format msgid "Priority Fee / Tip" msgstr "" @@ -2194,29 +2169,29 @@ msgstr "" msgid "RPC" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:473 +#: lib/block_scout_web/templates/transaction/overview.html.eex:546 #, elixir-autogen, elixir-format msgid "Raw Input" msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 -#: lib/block_scout_web/views/transaction_view.ex:535 +#: lib/block_scout_web/views/transaction_view.ex:554 #, elixir-autogen, elixir-format msgid "Raw Trace" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:89 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:380 -#: lib/block_scout_web/views/tokens/overview_view.ex:41 +#: lib/block_scout_web/views/address_view.ex:381 +#: lib/block_scout_web/views/tokens/overview_view.ex:42 #, elixir-autogen, elixir-format msgid "Read Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:96 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:381 +#: lib/block_scout_web/views/address_view.ex:382 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" @@ -2262,7 +2237,7 @@ msgstr "" msgid "Request to edit a public tag/label" msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Resend verification email" msgstr "" @@ -2319,7 +2294,7 @@ msgstr "" msgid "Save" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:206 +#: lib/block_scout_web/templates/transaction/overview.html.eex:226 #, elixir-autogen, elixir-format msgid "Scroll to see more" msgstr "" @@ -2459,7 +2434,7 @@ msgstr "" #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 #: lib/block_scout_web/templates/block_transaction/index.html.eex:14 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 -#: lib/block_scout_web/templates/chain/show.html.eex:158 +#: lib/block_scout_web/templates/chain/show.html.eex:160 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 @@ -2477,7 +2452,7 @@ msgstr "" msgid "Something went wrong, click to reload." msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:223 +#: lib/block_scout_web/templates/chain/show.html.eex:225 #, elixir-autogen, elixir-format msgid "Something went wrong, click to retry." msgstr "" @@ -2514,7 +2489,7 @@ msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 #: lib/block_scout_web/templates/transaction_state/index.html.eex:6 -#: lib/block_scout_web/views/transaction_view.ex:536 +#: lib/block_scout_web/views/transaction_view.ex:555 #, elixir-autogen, elixir-format msgid "State changes" msgstr "" @@ -2540,7 +2515,7 @@ msgid "Submit an Issue" msgstr "" #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 -#: lib/block_scout_web/views/transaction_view.ex:370 +#: lib/block_scout_web/views/transaction_view.ex:376 #, elixir-autogen, elixir-format msgid "Success" msgstr "" @@ -2551,11 +2526,6 @@ msgstr "" msgid "TX Fee" msgstr "" -#: lib/block_scout_web/templates/layout/_footer.html.eex:31 -#, elixir-autogen, elixir-format -msgid "Telegram" -msgstr "" - #: lib/block_scout_web/templates/layout/_footer.html.eex:67 #, elixir-autogen, elixir-format msgid "Test Networks" @@ -2776,12 +2746,12 @@ msgstr "" msgid "This block has not been processed yet." msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:48 +#: lib/block_scout_web/templates/address_contract/index.html.eex:47 #, elixir-autogen, elixir-format msgid "This contract has been partially verified via Sourcify." msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:52 +#: lib/block_scout_web/templates/address_contract/index.html.eex:51 #, elixir-autogen, elixir-format msgid "This contract has been verified via Sourcify." msgstr "" @@ -2816,7 +2786,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 #: lib/block_scout_web/templates/address_transaction/index.html.eex:28 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 -#: lib/block_scout_web/templates/transaction/overview.html.eex:247 +#: lib/block_scout_web/templates/transaction/overview.html.eex:267 #: lib/block_scout_web/templates/withdrawal/index.html.eex:32 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_token_transfer_view.ex:9 @@ -2849,13 +2819,13 @@ msgid "Token" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 -#: lib/block_scout_web/views/transaction_view.ex:469 +#: lib/block_scout_web/views/transaction_view.ex:488 #, elixir-autogen, elixir-format msgid "Token Burning" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 -#: lib/block_scout_web/views/transaction_view.ex:470 +#: lib/block_scout_web/views/transaction_view.ex:489 #, elixir-autogen, elixir-format msgid "Token Creation" msgstr "" @@ -2870,7 +2840,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 -#: lib/block_scout_web/views/tokens/overview_view.ex:40 +#: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Token Holders" msgstr "" @@ -2883,14 +2853,14 @@ msgid "Token ID" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 -#: lib/block_scout_web/views/transaction_view.ex:468 +#: lib/block_scout_web/views/transaction_view.ex:487 #, elixir-autogen, elixir-format msgid "Token Minting" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 -#: lib/block_scout_web/views/transaction_view.ex:471 +#: lib/block_scout_web/views/transaction_view.ex:490 #, elixir-autogen, elixir-format msgid "Token Transfer" msgstr "" @@ -2903,10 +2873,10 @@ msgstr "" #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:377 +#: lib/block_scout_web/views/address_view.ex:378 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:114 -#: lib/block_scout_web/views/tokens/overview_view.ex:39 -#: lib/block_scout_web/views/transaction_view.ex:532 +#: lib/block_scout_web/views/tokens/overview_view.ex:40 +#: lib/block_scout_web/views/transaction_view.ex:551 #, elixir-autogen, elixir-format msgid "Token Transfers" msgstr "" @@ -2927,27 +2897,27 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:84 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:374 +#: lib/block_scout_web/views/address_view.ex:375 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:316 +#: lib/block_scout_web/templates/transaction/overview.html.eex:336 #, elixir-autogen, elixir-format msgid "Tokens Burnt" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:332 +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 #, elixir-autogen, elixir-format msgid "Tokens Created" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:299 +#: lib/block_scout_web/templates/transaction/overview.html.eex:319 #, elixir-autogen, elixir-format msgid "Tokens Minted" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:283 +#: lib/block_scout_web/templates/transaction/overview.html.eex:303 #, elixir-autogen, elixir-format msgid "Tokens Transferred" msgstr "" @@ -2962,7 +2932,7 @@ msgstr "" msgid "Topic" msgstr "" -#: lib/block_scout_web/templates/address_logs/_logs.html.eex:71 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:91 #, elixir-autogen, elixir-format msgid "Topics" @@ -2989,7 +2959,7 @@ msgstr "" msgid "Total Supply * Price" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:131 +#: lib/block_scout_web/templates/chain/show.html.eex:133 #, elixir-autogen, elixir-format msgid "Total blocks" msgstr "" @@ -3009,12 +2979,12 @@ msgstr "" msgid "Total supply" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:363 +#: lib/block_scout_web/templates/transaction/overview.html.eex:383 #, elixir-autogen, elixir-format msgid "Total transaction fee." msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:110 +#: lib/block_scout_web/templates/chain/show.html.eex:112 #, elixir-autogen, elixir-format msgid "Total transactions" msgstr "" @@ -3022,7 +2992,7 @@ msgstr "" #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 -#: lib/block_scout_web/views/transaction_view.ex:481 +#: lib/block_scout_web/views/transaction_view.ex:500 #, elixir-autogen, elixir-format msgid "Transaction" msgstr "" @@ -3037,12 +3007,7 @@ msgstr "" msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:205 -#, elixir-autogen, elixir-format -msgid "Transaction Action" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:438 +#: lib/block_scout_web/templates/transaction/overview.html.eex:470 #, elixir-autogen, elixir-format msgid "Transaction Burnt Fee" msgstr "" @@ -3052,7 +3017,7 @@ msgstr "" msgid "Transaction Details" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:364 +#: lib/block_scout_web/templates/transaction/overview.html.eex:384 #, elixir-autogen, elixir-format msgid "Transaction Fee" msgstr "" @@ -3075,31 +3040,32 @@ msgstr "" msgid "Transaction Tags" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:388 +#: lib/block_scout_web/templates/transaction/overview.html.eex:414 #, elixir-autogen, elixir-format msgid "Transaction Type" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:461 +#: lib/block_scout_web/templates/transaction/overview.html.eex:534 #, elixir-autogen, elixir-format msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:387 +#: lib/block_scout_web/templates/transaction/overview.html.eex:413 #, elixir-autogen, elixir-format msgid "Transaction type, introduced in EIP-2718." msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tile.html.eex:31 #: lib/block_scout_web/templates/address/overview.html.eex:186 #: lib/block_scout_web/templates/address/overview.html.eex:192 #: lib/block_scout_web/templates/address/overview.html.eex:200 #: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/block/_tabs.html.eex:4 #: lib/block_scout_web/templates/block/overview.html.eex:80 -#: lib/block_scout_web/templates/chain/show.html.eex:214 +#: lib/block_scout_web/templates/chain/show.html.eex:216 #: lib/block_scout_web/templates/layout/_topnav.html.eex:49 -#: lib/block_scout_web/views/address_view.ex:376 +#: lib/block_scout_web/views/address_view.ex:377 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" @@ -3109,11 +3075,6 @@ msgstr "" msgid "Transactions and address of creation." msgstr "" -#: lib/block_scout_web/templates/address/_tile.html.eex:31 -#, elixir-autogen, elixir-format -msgid "Transactions sent" -msgstr "" - #: lib/block_scout_web/templates/address/overview.html.eex:213 #: lib/block_scout_web/templates/address/overview.html.eex:219 #: lib/block_scout_web/templates/address/overview.html.eex:227 @@ -3165,7 +3126,7 @@ msgstr "" msgid "UML diagram" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:487 +#: lib/block_scout_web/templates/transaction/overview.html.eex:560 #, elixir-autogen, elixir-format msgid "UTF-8" msgstr "" @@ -3181,7 +3142,7 @@ msgstr "" msgid "Uncles" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:361 +#: lib/block_scout_web/views/transaction_view.ex:367 #, elixir-autogen, elixir-format msgid "Unconfirmed" msgstr "" @@ -3202,12 +3163,12 @@ msgstr "" msgid "Update" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:417 +#: lib/block_scout_web/templates/transaction/overview.html.eex:449 #, elixir-autogen, elixir-format msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:427 +#: lib/block_scout_web/templates/transaction/overview.html.eex:459 #, elixir-autogen, elixir-format msgid "User-defined tip sent to validator for transaction priority/inclusion." msgstr "" @@ -3242,19 +3203,12 @@ msgstr "" msgid "Validator Name" msgstr "" -#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 -#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 -#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 -#, elixir-autogen, elixir-format -msgid "Validator index" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:349 +#: lib/block_scout_web/templates/transaction/overview.html.eex:369 #, elixir-autogen, elixir-format msgid "Value" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:348 +#: lib/block_scout_web/templates/transaction/overview.html.eex:368 #, elixir-autogen, elixir-format msgid "Value sent in the native token (and USD) if applicable." msgstr "" @@ -3272,7 +3226,7 @@ msgstr "" msgid "Verified Contracts" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:89 +#: lib/block_scout_web/templates/address_contract/index.html.eex:88 #, elixir-autogen, elixir-format msgid "Verified at" msgstr "" @@ -3292,10 +3246,10 @@ msgstr "" msgid "Verified contracts, %{subnetwork}, %{coin}" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:28 -#: lib/block_scout_web/templates/address_contract/index.html.eex:30 -#: lib/block_scout_web/templates/address_contract/index.html.eex:198 -#: lib/block_scout_web/templates/address_contract/index.html.eex:229 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:228 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 #, elixir-autogen, elixir-format msgid "Verify & Publish" @@ -3343,12 +3297,12 @@ msgstr "" msgid "Via multi-part files" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:153 +#: lib/block_scout_web/templates/chain/show.html.eex:155 #, elixir-autogen, elixir-format msgid "View All Blocks" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:213 +#: lib/block_scout_web/templates/chain/show.html.eex:215 #, elixir-autogen, elixir-format msgid "View All Transactions" msgstr "" @@ -3426,7 +3380,7 @@ msgstr "" msgid "Waiting for transaction's confirmation..." msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:139 +#: lib/block_scout_web/templates/chain/show.html.eex:141 #, elixir-autogen, elixir-format msgid "Wallet addresses" msgstr "" @@ -3448,7 +3402,7 @@ msgstr "" msgid "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" msgstr "" -#: lib/block_scout_web/views/wei_helper.ex:76 +#: lib/block_scout_web/views/wei_helper.ex:80 #, elixir-autogen, elixir-format msgid "Wei" msgstr "" @@ -3469,14 +3423,14 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:103 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:382 +#: lib/block_scout_web/views/address_view.ex:383 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:110 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:383 +#: lib/block_scout_web/views/address_view.ex:384 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" @@ -3535,6 +3489,12 @@ msgstr "" msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 +#: lib/block_scout_web/views/verified_contracts_view.ex:12 +#, elixir-autogen, elixir-format +msgid "Yul" +msgstr "" + #: lib/block_scout_web/templates/address/overview.html.eex:111 #, elixir-autogen, elixir-format msgid "at" @@ -3547,20 +3507,20 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:437 #, elixir-autogen, elixir-format -msgid "burned for this transaction. Equals Block Base Fee per Gas * Gas Used." +msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:215 #, elixir-autogen, elixir-format -msgid "burned from transactions included in the block (Base fee (per unit of gas) * Gas Used)." +msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)." msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:28 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "button" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:256 +#: lib/block_scout_web/templates/transaction/overview.html.eex:276 #, elixir-autogen, elixir-format msgid "created" msgstr "" @@ -3585,11 +3545,16 @@ msgstr "" msgid "fallback" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:26 +#: lib/block_scout_web/views/address_contract_view.ex:30 #, elixir-autogen, elixir-format msgid "false" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "for address" +msgstr "" + #: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 @@ -3612,7 +3577,7 @@ msgstr "" msgid "of" msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "on sign up. Didn’t receive?" msgstr "" @@ -3641,7 +3606,12 @@ msgstr "" msgid "string" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:25 +#: lib/block_scout_web/templates/csv_export/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "to CSV file" +msgstr "" + +#: lib/block_scout_web/views/address_contract_view.ex:29 #, elixir-autogen, elixir-format msgid "true" msgstr "" @@ -3651,6 +3621,88 @@ msgstr "" msgid "truffle flattener" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:484 +#, elixir-autogen, elixir-format +msgid "Actual gas amount used by the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:203 +#, elixir-autogen, elixir-format +msgid "Block number containing the transaction on L1." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:204 +#, elixir-autogen, elixir-format +msgid "L1 Block" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:523 +#: lib/block_scout_web/templates/transaction/overview.html.eex:524 +#, elixir-autogen, elixir-format +msgid "L1 Fee Scalar" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:512 +#: lib/block_scout_web/templates/transaction/overview.html.eex:513 +#, elixir-autogen, elixir-format +msgid "L1 Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:501 +#: lib/block_scout_web/templates/transaction/overview.html.eex:502 +#, elixir-autogen, elixir-format +msgid "L1 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 +#, elixir-autogen, elixir-format +msgid "L2 Gas Limit" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:400 +#, elixir-autogen, elixir-format +msgid "L2 Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:485 +#, elixir-autogen, elixir-format +msgid "L2 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 +#, elixir-autogen, elixir-format +msgid "Maximum gas amount approved for the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:399 +#, elixir-autogen, elixir-format +msgid "Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage." +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 +#, elixir-autogen, elixir-format +msgid "Amount" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Beacon chain withdrawals - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 +#, elixir-autogen, elixir-format +msgid "Index" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification via Standard input JSON" @@ -3661,28 +3713,54 @@ msgstr "" msgid "New Smart Contract Verification via metadata JSON" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:31 +#, elixir-autogen, elixir-format +msgid "Telegram" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:225 +#, elixir-autogen, elixir-format +msgid "Transaction Action" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 +#, elixir-autogen, elixir-format +msgid "Validator index" +msgstr "" + #: lib/block_scout_web/templates/withdrawal/index.html.eex:11 #, elixir-autogen, elixir-format msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." msgstr "" -#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#: lib/block_scout_web/templates/transaction/overview.html.eex:488 #, elixir-autogen, elixir-format -msgid "Export" +msgid "Actual gas amount used by the transaction." msgstr "" -#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#: lib/block_scout_web/templates/chain/show.html.eex:102 #, elixir-autogen, elixir-format -msgid "for address" +msgid "Average block time" msgstr "" -#: lib/block_scout_web/templates/csv_export/index.html.eex:17 +#: lib/block_scout_web/templates/transaction/overview.html.eex:404 #, elixir-autogen, elixir-format -msgid "to CSV file" +msgid "Gas Price" msgstr "" -#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 -#: lib/block_scout_web/views/verified_contracts_view.ex:12 +#: lib/block_scout_web/templates/transaction/overview.html.eex:489 #, elixir-autogen, elixir-format -msgid "Yul" +msgid "Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:429 +#, elixir-autogen, elixir-format +msgid "Maximum gas amount approved for the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#, elixir-autogen, elixir-format +msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index d770bca3428d..b6e1f7825b65 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -1,16 +1,15 @@ -#: lib/block_scout_web/views/address_token_balance_view.ex:10 -#, elixir-autogen, elixir-format -msgid "%{count} token" -msgid_plural "%{count} tokens" -msgstr[0] "" -msgstr[1] "" - -#: lib/block_scout_web/templates/block/_tile.html.eex:29 -#, elixir-autogen, elixir-format -msgid "%{count} transaction" -msgid_plural "%{count} transactions" -msgstr[0] "" -msgstr[1] "" +## "msgid"s in this file come from POT (.pot) files. +### +### Do not add, change, or remove "msgid"s manually here as +### they're tied to the ones in the corresponding POT file +### (with the same domain). +### +### Use "mix gettext.extract --merge" or "mix gettext.merge" +### to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 #, elixir-autogen, elixir-format @@ -53,6 +52,20 @@ msgstr "" msgid "%{count} Transactions" msgstr "" +#: lib/block_scout_web/views/address_token_balance_view.ex:10 +#, elixir-autogen, elixir-format +msgid "%{count} token" +msgid_plural "%{count} tokens" +msgstr[0] "" +msgstr[1] "" + +#: lib/block_scout_web/templates/block/_tile.html.eex:29 +#, elixir-autogen, elixir-format +msgid "%{count} transaction" +msgid_plural "%{count} transactions" +msgstr[0] "" +msgstr[1] "" + #: lib/block_scout_web/templates/transaction/_actions.html.eex:101 #, elixir-autogen, elixir-format msgid "%{qty} of Token ID [%{link_to_id}]" @@ -68,7 +81,7 @@ msgstr "" msgid "%{subnetwork} Explorer - BlockScout" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:369 +#: lib/block_scout_web/views/transaction_view.ex:375 #, elixir-autogen, elixir-format msgid "(Awaiting internal transactions for status)" msgstr "" @@ -86,7 +99,7 @@ msgstr "" msgid ") may be added for each contract. Click the Add Library button to add an additional one." msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:90 +#: lib/block_scout_web/templates/layout/app.html.eex:93 #, elixir-autogen, elixir-format msgid "- We're indexing this chain right now. Some of the counts may be inaccurate." msgstr "" @@ -122,7 +135,7 @@ msgstr "" msgid "A block producer who successfully included the block onto the blockchain." msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "A confirmation email was sent to" msgstr "" @@ -199,11 +212,6 @@ msgstr "" msgid "Actions" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:451 -#, elixir-autogen, elixir-format -msgid "Actual gas amount used by the transaction." -msgstr "" - #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 @@ -265,17 +273,17 @@ msgstr "" #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20 #: lib/block_scout_web/templates/transaction_state/index.html.eex:34 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 -#: lib/block_scout_web/views/address_view.ex:108 +#: lib/block_scout_web/views/address_view.ex:109 #, elixir-autogen, elixir-format msgid "Address" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:243 +#: lib/block_scout_web/templates/transaction/overview.html.eex:263 #, elixir-autogen, elixir-format msgid "Address (external or contract) receiving the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:225 +#: lib/block_scout_web/templates/transaction/overview.html.eex:245 #, elixir-autogen, elixir-format msgid "Address (external or contract) sending the transaction." msgstr "" @@ -336,7 +344,7 @@ msgstr "" msgid "All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:28 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "All metadata displayed below is from that contract. In order to verify current contract, click" msgstr "" @@ -346,14 +354,7 @@ msgstr "" msgid "All tokens in the account and total value." msgstr "" -#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 -#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 -#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 -#, elixir-autogen, elixir-format -msgid "Amount" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:437 +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 #, elixir-autogen, elixir-format msgid "Amount of" msgstr "" @@ -383,11 +384,6 @@ msgstr "" msgid "Average" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:101 -#, elixir-autogen, elixir-format -msgid "Average block time" -msgstr "" - #: lib/block_scout_web/templates/account/api_key/form.html.eex:25 #, elixir-autogen, elixir-format msgid "Back to API keys (Cancel)" @@ -455,17 +451,7 @@ msgstr "" msgid "Base URL:" msgstr "" -#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 -#, elixir-autogen, elixir-format -msgid "Beacon chain withdrawals - %{subnetwork} Explorer" -msgstr "" - -#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:472 +#: lib/block_scout_web/templates/transaction/overview.html.eex:545 #, elixir-autogen, elixir-format msgid "Binary data included with the transaction. See input / logs below for additional info." msgstr "" @@ -541,7 +527,7 @@ msgstr "" msgid "Blockchain" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:154 +#: lib/block_scout_web/templates/chain/show.html.eex:156 #: lib/block_scout_web/templates/layout/_topnav.html.eex:34 #: lib/block_scout_web/templates/layout/_topnav.html.eex:38 #, elixir-autogen, elixir-format @@ -556,7 +542,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/overview.html.eex:275 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:385 +#: lib/block_scout_web/views/address_view.ex:386 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" @@ -656,13 +642,13 @@ msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:378 +#: lib/block_scout_web/views/address_view.ex:379 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 -#: lib/block_scout_web/views/address_view.ex:384 +#: lib/block_scout_web/views/address_view.ex:385 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" @@ -688,17 +674,17 @@ msgstr "" msgid "Compiler" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:143 +#: lib/block_scout_web/templates/address_contract/index.html.eex:142 #, elixir-autogen, elixir-format msgid "Compiler Settings" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:72 +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 #, elixir-autogen, elixir-format msgid "Compiler version" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:362 +#: lib/block_scout_web/views/transaction_view.ex:368 #, elixir-autogen, elixir-format msgid "Confirmed" msgstr "" @@ -747,7 +733,7 @@ msgstr "" msgid "Connection Lost, click to load newer validations" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:97 +#: lib/block_scout_web/templates/address_contract/index.html.eex:96 #, elixir-autogen, elixir-format msgid "Constructor Arguments" msgstr "" @@ -758,12 +744,12 @@ msgid "Constructor args" msgstr "" #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 -#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 #, elixir-autogen, elixir-format msgid "Contract" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:158 +#: lib/block_scout_web/templates/address_contract/index.html.eex:157 #, elixir-autogen, elixir-format msgid "Contract ABI" msgstr "" @@ -771,30 +757,30 @@ msgstr "" #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 -#: lib/block_scout_web/views/address_view.ex:106 +#: lib/block_scout_web/views/address_view.ex:107 #, elixir-autogen, elixir-format msgid "Contract Address" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 -#: lib/block_scout_web/views/address_view.ex:46 -#: lib/block_scout_web/views/address_view.ex:80 +#: lib/block_scout_web/views/address_view.ex:47 +#: lib/block_scout_web/views/address_view.ex:81 #, elixir-autogen, elixir-format msgid "Contract Address Pending" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:478 +#: lib/block_scout_web/views/transaction_view.ex:497 #, elixir-autogen, elixir-format msgid "Contract Call" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:475 +#: lib/block_scout_web/views/transaction_view.ex:494 #, elixir-autogen, elixir-format msgid "Contract Creation" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:175 -#: lib/block_scout_web/templates/address_contract/index.html.eex:190 +#: lib/block_scout_web/templates/address_contract/index.html.eex:174 +#: lib/block_scout_web/templates/address_contract/index.html.eex:189 #, elixir-autogen, elixir-format msgid "Contract Creation Code" msgstr "" @@ -811,7 +797,7 @@ msgstr "" msgid "Contract Name" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:26 +#: lib/block_scout_web/templates/address_contract/index.html.eex:25 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 #, elixir-autogen, elixir-format msgid "Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB" @@ -822,12 +808,12 @@ msgstr "" msgid "Contract name or address" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:64 +#: lib/block_scout_web/templates/address_contract/index.html.eex:63 #, elixir-autogen, elixir-format msgid "Contract name:" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:107 +#: lib/block_scout_web/templates/address_contract/index.html.eex:106 #, elixir-autogen, elixir-format msgid "Contract source code" msgstr "" @@ -842,7 +828,7 @@ msgstr "" msgid "Contracts" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:181 +#: lib/block_scout_web/templates/address_contract/index.html.eex:180 #, elixir-autogen, elixir-format msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." msgstr "" @@ -852,7 +838,7 @@ msgstr "" msgid "Contribute" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:160 +#: lib/block_scout_web/templates/address_contract/index.html.eex:159 #, elixir-autogen, elixir-format msgid "Copy ABI" msgstr "" @@ -878,7 +864,7 @@ msgstr "" msgid "Copy Address" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:145 +#: lib/block_scout_web/templates/address_contract/index.html.eex:144 #, elixir-autogen, elixir-format msgid "Copy Compiler Settings" msgstr "" @@ -889,8 +875,8 @@ msgstr "" msgid "Copy Contract Address" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:177 -#: lib/block_scout_web/templates/address_contract/index.html.eex:193 +#: lib/block_scout_web/templates/address_contract/index.html.eex:176 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 #, elixir-autogen, elixir-format msgid "Copy Contract Creation Code" msgstr "" @@ -900,8 +886,8 @@ msgstr "" msgid "Copy Decompiled Contract Code" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:214 -#: lib/block_scout_web/templates/address_contract/index.html.eex:224 +#: lib/block_scout_web/templates/address_contract/index.html.eex:213 +#: lib/block_scout_web/templates/address_contract/index.html.eex:223 #, elixir-autogen, elixir-format msgid "Copy Deployed ByteCode" msgstr "" @@ -909,8 +895,8 @@ msgstr "" #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 -#: lib/block_scout_web/templates/transaction/overview.html.eex:233 -#: lib/block_scout_web/templates/transaction/overview.html.eex:234 +#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:254 #, elixir-autogen, elixir-format msgid "Copy From Address" msgstr "" @@ -937,18 +923,18 @@ msgstr "" msgid "Copy Raw Trace" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:121 -#: lib/block_scout_web/templates/address_contract/index.html.eex:133 +#: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:132 #, elixir-autogen, elixir-format msgid "Copy Source Code" msgstr "" #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 -#: lib/block_scout_web/templates/transaction/overview.html.eex:260 -#: lib/block_scout_web/templates/transaction/overview.html.eex:261 -#: lib/block_scout_web/templates/transaction/overview.html.eex:268 -#: lib/block_scout_web/templates/transaction/overview.html.eex:269 +#: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:281 +#: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 #, elixir-autogen, elixir-format msgid "Copy To Address" msgstr "" @@ -969,20 +955,20 @@ msgstr "" msgid "Copy Txn Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:498 +#: lib/block_scout_web/templates/transaction/overview.html.eex:571 #, elixir-autogen, elixir-format msgid "Copy Txn Hex Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:504 +#: lib/block_scout_web/templates/transaction/overview.html.eex:577 #, elixir-autogen, elixir-format msgid "Copy Txn UTF-8 Input" msgstr "" #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 -#: lib/block_scout_web/templates/transaction/overview.html.eex:497 -#: lib/block_scout_web/templates/transaction/overview.html.eex:503 +#: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:576 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 #, elixir-autogen, elixir-format msgid "Copy Value" @@ -1049,7 +1035,7 @@ msgstr "" msgid "Daily Transactions" msgstr "" -#: lib/block_scout_web/templates/address_logs/_logs.html.eex:101 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:121 @@ -1084,7 +1070,7 @@ msgstr "" msgid "Decoded" msgstr "" -#: lib/block_scout_web/views/address_view.ex:379 +#: lib/block_scout_web/views/address_view.ex:380 #, elixir-autogen, elixir-format msgid "Decompiled Code" msgstr "" @@ -1109,8 +1095,8 @@ msgstr "" msgid "Delegate Call" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:212 -#: lib/block_scout_web/templates/address_contract/index.html.eex:220 +#: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:219 #, elixir-autogen, elixir-format msgid "Deployed ByteCode" msgstr "" @@ -1140,7 +1126,7 @@ msgstr "" msgid "Difficulty" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:182 +#: lib/block_scout_web/templates/address_contract/index.html.eex:181 #, elixir-autogen, elixir-format msgid "Displaying the init data provided of the creating transaction." msgstr "" @@ -1186,12 +1172,12 @@ msgstr "" msgid "EIP-1167" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:220 +#: lib/block_scout_web/views/transaction_view.ex:225 #, elixir-autogen, elixir-format msgid "ERC-1155 " msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:218 +#: lib/block_scout_web/views/transaction_view.ex:223 #, elixir-autogen, elixir-format msgid "ERC-20 " msgstr "" @@ -1201,7 +1187,7 @@ msgstr "" msgid "ERC-20 tokens (beta)" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:219 +#: lib/block_scout_web/views/transaction_view.ex:224 #, elixir-autogen, elixir-format msgid "ERC-721 " msgstr "" @@ -1216,7 +1202,7 @@ msgstr "" msgid "ETH RPC API Documentation" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:83 +#: lib/block_scout_web/templates/address_contract/index.html.eex:82 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 #, elixir-autogen, elixir-format @@ -1292,12 +1278,12 @@ msgstr "" msgid "Error trying to fetch balances." msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:373 +#: lib/block_scout_web/views/transaction_view.ex:379 #, elixir-autogen, elixir-format msgid "Error: %{reason}" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:371 +#: lib/block_scout_web/views/transaction_view.ex:377 #, elixir-autogen, elixir-format msgid "Error: (Awaiting internal transactions for reason)" msgstr "" @@ -1329,12 +1315,17 @@ msgstr "" msgid "Expand" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "Export" +msgstr "" + #: lib/block_scout_web/templates/csv_export/index.html.eex:10 #, elixir-autogen, elixir-format msgid "Export Data" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:249 +#: lib/block_scout_web/templates/address_contract/index.html.eex:248 #, elixir-autogen, elixir-format msgid "External libraries" msgstr "" @@ -1411,7 +1402,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 #: lib/block_scout_web/templates/address_transaction/index.html.eex:34 -#: lib/block_scout_web/templates/transaction/overview.html.eex:226 +#: lib/block_scout_web/templates/transaction/overview.html.eex:246 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10 @@ -1426,16 +1417,11 @@ msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/overview.html.eex:187 -#: lib/block_scout_web/templates/transaction/overview.html.eex:399 +#: lib/block_scout_web/templates/transaction/overview.html.eex:430 #, elixir-autogen, elixir-format msgid "Gas Limit" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:379 -#, elixir-autogen, elixir-format -msgid "Gas Price" -msgstr "" - #: lib/block_scout_web/templates/address/overview.html.eex:240 #: lib/block_scout_web/templates/block/_tile.html.eex:73 #: lib/block_scout_web/templates/block/overview.html.eex:178 @@ -1443,11 +1429,6 @@ msgstr "" msgid "Gas Used" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:452 -#, elixir-autogen, elixir-format -msgid "Gas Used by Transaction" -msgstr "" - #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 #, elixir-autogen, elixir-format @@ -1485,7 +1466,7 @@ msgstr "" #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 #: lib/block_scout_web/views/block_view.ex:22 -#: lib/block_scout_web/views/wei_helper.ex:77 +#: lib/block_scout_web/views/wei_helper.ex:81 #, elixir-autogen, elixir-format msgid "Gwei" msgstr "" @@ -1495,13 +1476,13 @@ msgstr "" msgid "Hash" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:480 -#: lib/block_scout_web/templates/transaction/overview.html.eex:484 +#: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:557 #, elixir-autogen, elixir-format msgid "Hex (Default)" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:204 +#: lib/block_scout_web/templates/transaction/overview.html.eex:224 #, elixir-autogen, elixir-format msgid "Highlighted events of the transaction." msgstr "" @@ -1560,14 +1541,7 @@ msgstr "" msgid "Incoming" msgstr "" -#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 -#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 -#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 -#, elixir-autogen, elixir-format -msgid "Index" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:464 +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Index position of Transaction in the block." msgstr "" @@ -1587,7 +1561,7 @@ msgstr "" msgid "Input" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:245 +#: lib/block_scout_web/templates/transaction/overview.html.eex:265 #, elixir-autogen, elixir-format msgid "Interacted With (To)" msgstr "" @@ -1601,8 +1575,8 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:375 -#: lib/block_scout_web/views/transaction_view.ex:533 +#: lib/block_scout_web/views/address_view.ex:376 +#: lib/block_scout_web/views/transaction_view.ex:552 #, elixir-autogen, elixir-format msgid "Internal Transactions" msgstr "" @@ -1614,7 +1588,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 -#: lib/block_scout_web/views/tokens/overview_view.ex:42 +#: lib/block_scout_web/views/tokens/overview_view.ex:43 #, elixir-autogen, elixir-format msgid "Inventory" msgstr "" @@ -1662,22 +1636,22 @@ msgstr "" msgid "License ID" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:331 +#: lib/block_scout_web/templates/transaction/overview.html.eex:351 #, elixir-autogen, elixir-format msgid "List of ERC-1155 tokens created in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:315 +#: lib/block_scout_web/templates/transaction/overview.html.eex:335 #, elixir-autogen, elixir-format msgid "List of token burnt in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:298 +#: lib/block_scout_web/templates/transaction/overview.html.eex:318 #, elixir-autogen, elixir-format msgid "List of token minted in the transaction." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:282 +#: lib/block_scout_web/templates/transaction/overview.html.eex:302 #, elixir-autogen, elixir-format msgid "List of token transferred in the transaction." msgstr "" @@ -1718,8 +1692,8 @@ msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:386 -#: lib/block_scout_web/views/transaction_view.ex:534 +#: lib/block_scout_web/views/address_view.ex:387 +#: lib/block_scout_web/views/transaction_view.ex:553 #, elixir-autogen, elixir-format msgid "Logs" msgstr "" @@ -1732,7 +1706,7 @@ msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:53 #: lib/block_scout_web/templates/layout/app.html.eex:50 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 -#: lib/block_scout_web/views/address_view.ex:146 +#: lib/block_scout_web/views/address_view.ex:147 #, elixir-autogen, elixir-format msgid "Market Cap" msgstr "" @@ -1742,27 +1716,22 @@ msgstr "" msgid "Market cap" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:408 +#: lib/block_scout_web/templates/transaction/overview.html.eex:440 #, elixir-autogen, elixir-format msgid "Max Fee per Gas" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:418 +#: lib/block_scout_web/templates/transaction/overview.html.eex:450 #, elixir-autogen, elixir-format msgid "Max Priority Fee per Gas" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:325 +#: lib/block_scout_web/views/transaction_view.ex:331 #, elixir-autogen, elixir-format msgid "Max of" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:398 -#, elixir-autogen, elixir-format -msgid "Maximum gas amount approved for the transaction." -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:407 +#: lib/block_scout_web/templates/transaction/overview.html.eex:439 #, elixir-autogen, elixir-format msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." msgstr "" @@ -1824,7 +1793,7 @@ msgid "More internal transactions have come in" msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:46 -#: lib/block_scout_web/templates/chain/show.html.eex:217 +#: lib/block_scout_web/templates/chain/show.html.eex:219 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 #: lib/block_scout_web/templates/transaction/index.html.eex:19 #, elixir-autogen, elixir-format @@ -1904,6 +1873,16 @@ msgstr "" msgid "New Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via Standard input JSON" +msgstr "" + +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 +#, elixir-autogen, elixir-format +msgid "New Smart Contract Verification via metadata JSON" +msgstr "" + #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #, elixir-autogen, elixir-format @@ -1944,7 +1923,7 @@ msgid "No trace entries found." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:196 -#: lib/block_scout_web/templates/transaction/overview.html.eex:462 +#: lib/block_scout_web/templates/transaction/overview.html.eex:535 #, elixir-autogen, elixir-format msgid "Nonce" msgstr "" @@ -2002,12 +1981,12 @@ msgstr "" msgid "Optimization" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:68 +#: lib/block_scout_web/templates/address_contract/index.html.eex:67 #, elixir-autogen, elixir-format msgid "Optimization enabled" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:77 +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 #, elixir-autogen, elixir-format @@ -2064,8 +2043,9 @@ msgid "Parent Hash" msgstr "" #: lib/block_scout_web/templates/layout/_topnav.html.eex:63 -#: lib/block_scout_web/views/transaction_view.ex:368 -#: lib/block_scout_web/views/transaction_view.ex:407 +#: lib/block_scout_web/views/transaction_view.ex:373 +#: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:427 #, elixir-autogen, elixir-format msgid "Pending" msgstr "" @@ -2082,7 +2062,7 @@ msgid "Play" msgstr "" #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:22 -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Please confirm your email address to use the My Account feature." msgstr "" @@ -2097,7 +2077,7 @@ msgstr "" msgid "Please select what types of notifications you will receive:" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:464 +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Position" msgstr "" @@ -2134,13 +2114,8 @@ msgstr "" msgid "Price per token on the exchanges" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:378 -#, elixir-autogen, elixir-format -msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." -msgstr "" - #: lib/block_scout_web/templates/block/overview.html.eex:225 -#: lib/block_scout_web/templates/transaction/overview.html.eex:428 +#: lib/block_scout_web/templates/transaction/overview.html.eex:460 #, elixir-autogen, elixir-format msgid "Priority Fee / Tip" msgstr "" @@ -2194,29 +2169,29 @@ msgstr "" msgid "RPC" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:473 +#: lib/block_scout_web/templates/transaction/overview.html.eex:546 #, elixir-autogen, elixir-format msgid "Raw Input" msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 -#: lib/block_scout_web/views/transaction_view.ex:535 +#: lib/block_scout_web/views/transaction_view.ex:554 #, elixir-autogen, elixir-format msgid "Raw Trace" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:89 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:380 -#: lib/block_scout_web/views/tokens/overview_view.ex:41 +#: lib/block_scout_web/views/address_view.ex:381 +#: lib/block_scout_web/views/tokens/overview_view.ex:42 #, elixir-autogen, elixir-format msgid "Read Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:96 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:381 +#: lib/block_scout_web/views/address_view.ex:382 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" @@ -2262,7 +2237,7 @@ msgstr "" msgid "Request to edit a public tag/label" msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Resend verification email" msgstr "" @@ -2319,7 +2294,7 @@ msgstr "" msgid "Save" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:206 +#: lib/block_scout_web/templates/transaction/overview.html.eex:226 #, elixir-autogen, elixir-format msgid "Scroll to see more" msgstr "" @@ -2459,7 +2434,7 @@ msgstr "" #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 #: lib/block_scout_web/templates/block_transaction/index.html.eex:14 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 -#: lib/block_scout_web/templates/chain/show.html.eex:158 +#: lib/block_scout_web/templates/chain/show.html.eex:160 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 @@ -2477,7 +2452,7 @@ msgstr "" msgid "Something went wrong, click to reload." msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:223 +#: lib/block_scout_web/templates/chain/show.html.eex:225 #, elixir-autogen, elixir-format msgid "Something went wrong, click to retry." msgstr "" @@ -2514,7 +2489,7 @@ msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 #: lib/block_scout_web/templates/transaction_state/index.html.eex:6 -#: lib/block_scout_web/views/transaction_view.ex:536 +#: lib/block_scout_web/views/transaction_view.ex:555 #, elixir-autogen, elixir-format msgid "State changes" msgstr "" @@ -2540,7 +2515,7 @@ msgid "Submit an Issue" msgstr "" #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 -#: lib/block_scout_web/views/transaction_view.ex:370 +#: lib/block_scout_web/views/transaction_view.ex:376 #, elixir-autogen, elixir-format msgid "Success" msgstr "" @@ -2551,11 +2526,6 @@ msgstr "" msgid "TX Fee" msgstr "" -#: lib/block_scout_web/templates/layout/_footer.html.eex:31 -#, elixir-autogen, elixir-format -msgid "Telegram" -msgstr "" - #: lib/block_scout_web/templates/layout/_footer.html.eex:67 #, elixir-autogen, elixir-format msgid "Test Networks" @@ -2776,12 +2746,12 @@ msgstr "" msgid "This block has not been processed yet." msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:48 +#: lib/block_scout_web/templates/address_contract/index.html.eex:47 #, elixir-autogen, elixir-format msgid "This contract has been partially verified via Sourcify." msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:52 +#: lib/block_scout_web/templates/address_contract/index.html.eex:51 #, elixir-autogen, elixir-format msgid "This contract has been verified via Sourcify." msgstr "" @@ -2816,7 +2786,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 #: lib/block_scout_web/templates/address_transaction/index.html.eex:28 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 -#: lib/block_scout_web/templates/transaction/overview.html.eex:247 +#: lib/block_scout_web/templates/transaction/overview.html.eex:267 #: lib/block_scout_web/templates/withdrawal/index.html.eex:32 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_token_transfer_view.ex:9 @@ -2849,13 +2819,13 @@ msgid "Token" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 -#: lib/block_scout_web/views/transaction_view.ex:469 +#: lib/block_scout_web/views/transaction_view.ex:488 #, elixir-autogen, elixir-format msgid "Token Burning" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 -#: lib/block_scout_web/views/transaction_view.ex:470 +#: lib/block_scout_web/views/transaction_view.ex:489 #, elixir-autogen, elixir-format msgid "Token Creation" msgstr "" @@ -2870,7 +2840,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 -#: lib/block_scout_web/views/tokens/overview_view.ex:40 +#: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Token Holders" msgstr "" @@ -2883,14 +2853,14 @@ msgid "Token ID" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 -#: lib/block_scout_web/views/transaction_view.ex:468 +#: lib/block_scout_web/views/transaction_view.ex:487 #, elixir-autogen, elixir-format msgid "Token Minting" msgstr "" #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 -#: lib/block_scout_web/views/transaction_view.ex:471 +#: lib/block_scout_web/views/transaction_view.ex:490 #, elixir-autogen, elixir-format msgid "Token Transfer" msgstr "" @@ -2903,10 +2873,10 @@ msgstr "" #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:377 +#: lib/block_scout_web/views/address_view.ex:378 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:114 -#: lib/block_scout_web/views/tokens/overview_view.ex:39 -#: lib/block_scout_web/views/transaction_view.ex:532 +#: lib/block_scout_web/views/tokens/overview_view.ex:40 +#: lib/block_scout_web/views/transaction_view.ex:551 #, elixir-autogen, elixir-format msgid "Token Transfers" msgstr "" @@ -2927,27 +2897,27 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:84 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:374 +#: lib/block_scout_web/views/address_view.ex:375 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:316 +#: lib/block_scout_web/templates/transaction/overview.html.eex:336 #, elixir-autogen, elixir-format msgid "Tokens Burnt" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:332 +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 #, elixir-autogen, elixir-format msgid "Tokens Created" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:299 +#: lib/block_scout_web/templates/transaction/overview.html.eex:319 #, elixir-autogen, elixir-format msgid "Tokens Minted" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:283 +#: lib/block_scout_web/templates/transaction/overview.html.eex:303 #, elixir-autogen, elixir-format msgid "Tokens Transferred" msgstr "" @@ -2962,7 +2932,7 @@ msgstr "" msgid "Topic" msgstr "" -#: lib/block_scout_web/templates/address_logs/_logs.html.eex:71 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:91 #, elixir-autogen, elixir-format msgid "Topics" @@ -2989,7 +2959,7 @@ msgstr "" msgid "Total Supply * Price" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:131 +#: lib/block_scout_web/templates/chain/show.html.eex:133 #, elixir-autogen, elixir-format msgid "Total blocks" msgstr "" @@ -3009,12 +2979,12 @@ msgstr "" msgid "Total supply" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:363 +#: lib/block_scout_web/templates/transaction/overview.html.eex:383 #, elixir-autogen, elixir-format msgid "Total transaction fee." msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:110 +#: lib/block_scout_web/templates/chain/show.html.eex:112 #, elixir-autogen, elixir-format msgid "Total transactions" msgstr "" @@ -3022,7 +2992,7 @@ msgstr "" #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 -#: lib/block_scout_web/views/transaction_view.ex:481 +#: lib/block_scout_web/views/transaction_view.ex:500 #, elixir-autogen, elixir-format msgid "Transaction" msgstr "" @@ -3037,12 +3007,7 @@ msgstr "" msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:205 -#, elixir-autogen, elixir-format -msgid "Transaction Action" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:438 +#: lib/block_scout_web/templates/transaction/overview.html.eex:470 #, elixir-autogen, elixir-format msgid "Transaction Burnt Fee" msgstr "" @@ -3052,7 +3017,7 @@ msgstr "" msgid "Transaction Details" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:364 +#: lib/block_scout_web/templates/transaction/overview.html.eex:384 #, elixir-autogen, elixir-format msgid "Transaction Fee" msgstr "" @@ -3075,31 +3040,32 @@ msgstr "" msgid "Transaction Tags" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:388 +#: lib/block_scout_web/templates/transaction/overview.html.eex:414 #, elixir-autogen, elixir-format msgid "Transaction Type" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:461 +#: lib/block_scout_web/templates/transaction/overview.html.eex:534 #, elixir-autogen, elixir-format msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:387 +#: lib/block_scout_web/templates/transaction/overview.html.eex:413 #, elixir-autogen, elixir-format msgid "Transaction type, introduced in EIP-2718." msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tile.html.eex:31 #: lib/block_scout_web/templates/address/overview.html.eex:186 #: lib/block_scout_web/templates/address/overview.html.eex:192 #: lib/block_scout_web/templates/address/overview.html.eex:200 #: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/block/_tabs.html.eex:4 #: lib/block_scout_web/templates/block/overview.html.eex:80 -#: lib/block_scout_web/templates/chain/show.html.eex:214 +#: lib/block_scout_web/templates/chain/show.html.eex:216 #: lib/block_scout_web/templates/layout/_topnav.html.eex:49 -#: lib/block_scout_web/views/address_view.ex:376 +#: lib/block_scout_web/views/address_view.ex:377 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" @@ -3109,11 +3075,6 @@ msgstr "" msgid "Transactions and address of creation." msgstr "" -#: lib/block_scout_web/templates/address/_tile.html.eex:31 -#, elixir-autogen, elixir-format -msgid "Transactions sent" -msgstr "" - #: lib/block_scout_web/templates/address/overview.html.eex:213 #: lib/block_scout_web/templates/address/overview.html.eex:219 #: lib/block_scout_web/templates/address/overview.html.eex:227 @@ -3165,7 +3126,7 @@ msgstr "" msgid "UML diagram" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:487 +#: lib/block_scout_web/templates/transaction/overview.html.eex:560 #, elixir-autogen, elixir-format msgid "UTF-8" msgstr "" @@ -3181,7 +3142,7 @@ msgstr "" msgid "Uncles" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:361 +#: lib/block_scout_web/views/transaction_view.ex:367 #, elixir-autogen, elixir-format msgid "Unconfirmed" msgstr "" @@ -3202,12 +3163,12 @@ msgstr "" msgid "Update" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:417 +#: lib/block_scout_web/templates/transaction/overview.html.eex:449 #, elixir-autogen, elixir-format msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:427 +#: lib/block_scout_web/templates/transaction/overview.html.eex:459 #, elixir-autogen, elixir-format msgid "User-defined tip sent to validator for transaction priority/inclusion." msgstr "" @@ -3242,19 +3203,12 @@ msgstr "" msgid "Validator Name" msgstr "" -#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 -#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 -#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 -#, elixir-autogen, elixir-format -msgid "Validator index" -msgstr "" - -#: lib/block_scout_web/templates/transaction/overview.html.eex:349 +#: lib/block_scout_web/templates/transaction/overview.html.eex:369 #, elixir-autogen, elixir-format msgid "Value" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:348 +#: lib/block_scout_web/templates/transaction/overview.html.eex:368 #, elixir-autogen, elixir-format msgid "Value sent in the native token (and USD) if applicable." msgstr "" @@ -3272,7 +3226,7 @@ msgstr "" msgid "Verified Contracts" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:89 +#: lib/block_scout_web/templates/address_contract/index.html.eex:88 #, elixir-autogen, elixir-format msgid "Verified at" msgstr "" @@ -3292,10 +3246,10 @@ msgstr "" msgid "Verified contracts, %{subnetwork}, %{coin}" msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:28 -#: lib/block_scout_web/templates/address_contract/index.html.eex:30 -#: lib/block_scout_web/templates/address_contract/index.html.eex:198 -#: lib/block_scout_web/templates/address_contract/index.html.eex:229 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:228 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 #, elixir-autogen, elixir-format msgid "Verify & Publish" @@ -3343,12 +3297,12 @@ msgstr "" msgid "Via multi-part files" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:153 +#: lib/block_scout_web/templates/chain/show.html.eex:155 #, elixir-autogen, elixir-format msgid "View All Blocks" msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:213 +#: lib/block_scout_web/templates/chain/show.html.eex:215 #, elixir-autogen, elixir-format msgid "View All Transactions" msgstr "" @@ -3426,7 +3380,7 @@ msgstr "" msgid "Waiting for transaction's confirmation..." msgstr "" -#: lib/block_scout_web/templates/chain/show.html.eex:139 +#: lib/block_scout_web/templates/chain/show.html.eex:141 #, elixir-autogen, elixir-format msgid "Wallet addresses" msgstr "" @@ -3448,7 +3402,7 @@ msgstr "" msgid "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" msgstr "" -#: lib/block_scout_web/views/wei_helper.ex:76 +#: lib/block_scout_web/views/wei_helper.ex:80 #, elixir-autogen, elixir-format msgid "Wei" msgstr "" @@ -3469,14 +3423,14 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:103 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:382 +#: lib/block_scout_web/views/address_view.ex:383 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:110 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:383 +#: lib/block_scout_web/views/address_view.ex:384 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" @@ -3535,6 +3489,12 @@ msgstr "" msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 +#: lib/block_scout_web/views/verified_contracts_view.ex:12 +#, elixir-autogen, elixir-format +msgid "Yul" +msgstr "" + #: lib/block_scout_web/templates/address/overview.html.eex:111 #, elixir-autogen, elixir-format msgid "at" @@ -3547,20 +3507,20 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:437 #, elixir-autogen, elixir-format -msgid "burned for this transaction. Equals Block Base Fee per Gas * Gas Used." +msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:215 #, elixir-autogen, elixir-format -msgid "burned from transactions included in the block (Base fee (per unit of gas) * Gas Used)." +msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)." msgstr "" -#: lib/block_scout_web/templates/address_contract/index.html.eex:28 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "button" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:256 +#: lib/block_scout_web/templates/transaction/overview.html.eex:276 #, elixir-autogen, elixir-format msgid "created" msgstr "" @@ -3585,11 +3545,16 @@ msgstr "" msgid "fallback" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:26 +#: lib/block_scout_web/views/address_contract_view.ex:30 #, elixir-autogen, elixir-format msgid "false" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#, elixir-autogen, elixir-format +msgid "for address" +msgstr "" + #: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 @@ -3612,7 +3577,7 @@ msgstr "" msgid "of" msgstr "" -#: lib/block_scout_web/templates/layout/app.html.eex:97 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "on sign up. Didn’t receive?" msgstr "" @@ -3641,7 +3606,12 @@ msgstr "" msgid "string" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:25 +#: lib/block_scout_web/templates/csv_export/index.html.eex:17 +#, elixir-autogen, elixir-format +msgid "to CSV file" +msgstr "" + +#: lib/block_scout_web/views/address_contract_view.ex:29 #, elixir-autogen, elixir-format msgid "true" msgstr "" @@ -3651,38 +3621,129 @@ msgstr "" msgid "truffle flattener" msgstr "" -#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 +#: lib/block_scout_web/templates/transaction/overview.html.eex:484 +#, elixir-autogen, elixir-format +msgid "Actual gas amount used by the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:203 #, elixir-autogen, elixir-format, fuzzy -msgid "New Smart Contract Verification via Standard input JSON" +msgid "Block number containing the transaction on L1." msgstr "" -#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 +#: lib/block_scout_web/templates/transaction/overview.html.eex:204 #, elixir-autogen, elixir-format, fuzzy -msgid "New Smart Contract Verification via metadata JSON" +msgid "L1 Block" msgstr "" -#: lib/block_scout_web/templates/withdrawal/index.html.eex:11 +#: lib/block_scout_web/templates/transaction/overview.html.eex:523 +#: lib/block_scout_web/templates/transaction/overview.html.eex:524 +#, elixir-autogen, elixir-format +msgid "L1 Fee Scalar" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:512 +#: lib/block_scout_web/templates/transaction/overview.html.eex:513 #, elixir-autogen, elixir-format, fuzzy -msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." +msgid "L1 Gas Price" msgstr "" -#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#: lib/block_scout_web/templates/transaction/overview.html.eex:501 +#: lib/block_scout_web/templates/transaction/overview.html.eex:502 +#, elixir-autogen, elixir-format +msgid "L1 Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 #, elixir-autogen, elixir-format, fuzzy -msgid "Export" +msgid "L2 Gas Limit" msgstr "" -#: lib/block_scout_web/templates/csv_export/index.html.eex:14 +#: lib/block_scout_web/templates/transaction/overview.html.eex:400 #, elixir-autogen, elixir-format, fuzzy -msgid "for address" +msgid "L2 Gas Price" msgstr "" -#: lib/block_scout_web/templates/csv_export/index.html.eex:17 +#: lib/block_scout_web/templates/transaction/overview.html.eex:485 #, elixir-autogen, elixir-format -msgid "to CSV file" +msgid "L2 Gas Used by Transaction" msgstr "" -#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 -#: lib/block_scout_web/views/verified_contracts_view.ex:12 +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 #, elixir-autogen, elixir-format -msgid "Yul" +msgid "Maximum gas amount approved for the transaction on L2." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:399 +#, elixir-autogen, elixir-format +msgid "Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage." +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 +#, elixir-autogen, elixir-format, fuzzy +msgid "Amount" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 +#, elixir-autogen, elixir-format +msgid "Beacon chain withdrawals - %{subnetwork} Explorer" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 +#, elixir-autogen, elixir-format +msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" +msgstr "" + +#: lib/block_scout_web/templates/layout/_footer.html.eex:31 +#, elixir-autogen, elixir-format +msgid "Telegram" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:225 +#, elixir-autogen, elixir-format, fuzzy +msgid "Transaction Action" +msgstr "" + +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 +#, elixir-autogen, elixir-format, fuzzy +msgid "Validator index" +msgstr "" + +#: lib/block_scout_web/templates/withdrawal/index.html.eex:11 +#, elixir-autogen, elixir-format +msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:488 +#, elixir-autogen, elixir-format, fuzzy +msgid "Actual gas amount used by the transaction." +msgstr "" + +#: lib/block_scout_web/templates/chain/show.html.eex:102 +#, elixir-autogen, elixir-format +msgid "Average block time" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:404 +#, elixir-autogen, elixir-format, fuzzy +msgid "Gas Price" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:489 +#, elixir-autogen, elixir-format, fuzzy +msgid "Gas Used by Transaction" +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:429 +#, elixir-autogen, elixir-format, fuzzy +msgid "Maximum gas amount approved for the transaction." +msgstr "" + +#: lib/block_scout_web/templates/transaction/overview.html.eex:403 +#, elixir-autogen, elixir-format, fuzzy +msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs index 34f26785694d..7ae814d1be84 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs @@ -31,7 +31,8 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do name: "test", symbol: Explorer.coin(), usd_value: Decimal.new("2.5"), - volume_24h_usd: Decimal.new("1000.0") + volume_24h_usd: Decimal.new("1000.0"), + image_url: nil } on_exit(fn -> diff --git a/apps/block_scout_web/test/block_scout_web/channels/websocket_v2_test.exs b/apps/block_scout_web/test/block_scout_web/channels/websocket_v2_test.exs index 918b636cb3e8..e4ff798de0a1 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/websocket_v2_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/websocket_v2_test.exs @@ -6,7 +6,15 @@ defmodule BlockScoutWeb.WebsocketV2Test do alias Explorer.Chain.{Address, Import, Token, TokenTransfer, Transaction} alias Explorer.Repo + @first_topic_hex_string "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @second_topic_hex_string "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + @third_topic_hex_string "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d" + describe "websocket v2" do + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string) + {:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string) + {:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string) + @import_data %{ blocks: %{ params: [ @@ -34,37 +42,34 @@ defmodule BlockScoutWeb.WebsocketV2Test do block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, fourth_topic: nil, index: 0, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" }, %{ block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, fourth_topic: nil, index: 1, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" }, %{ block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, fourth_topic: nil, index: 2, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } ], timeout: 5 @@ -74,6 +79,7 @@ defmodule BlockScoutWeb.WebsocketV2Test do %{ block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", block_number: 37, + block_timestamp: Timex.parse!("2017-12-15T21:06:30.000000Z", "{ISO:Extended:Z}"), cumulative_gas_used: 50450, from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", gas: 4_700_000, @@ -96,6 +102,7 @@ defmodule BlockScoutWeb.WebsocketV2Test do %{ block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", block_number: 37, + block_timestamp: Timex.parse!("2017-12-15T21:06:30.000000Z", "{ISO:Extended:Z}"), cumulative_gas_used: 50450, from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", gas: 4_700_000, @@ -183,6 +190,7 @@ defmodule BlockScoutWeb.WebsocketV2Test do from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + token_type: "ERC-20", transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" }, %{ @@ -193,6 +201,7 @@ defmodule BlockScoutWeb.WebsocketV2Test do from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + token_type: "ERC-20", transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" }, %{ @@ -203,6 +212,7 @@ defmodule BlockScoutWeb.WebsocketV2Test do from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x00f38d4764929064f2d4d3a56520a76ab3df4151", + token_type: "ERC-20", transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } ], diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs index 2a8639bfdc62..378336b35fc4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs @@ -156,7 +156,8 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do "name" => nil, "private_tags" => [], "public_tags" => [], - "watchlist_names" => [] + "watchlist_names" => [], + "ens_domain_name" => nil } }} end) @@ -207,7 +208,8 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do "name" => nil, "private_tags" => [], "public_tags" => [], - "watchlist_names" => [] + "watchlist_names" => [], + "ens_domain_name" => nil } }} end) @@ -554,7 +556,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do response_1 = conn - |> get("/api/account/v2/user/watchlist", response["next_page_params"] |> dbg()) + |> get("/api/account/v2/user/watchlist", response["next_page_params"]) |> json_response(200) check_paginated_response(response, response_1, tags_address) @@ -1216,6 +1218,10 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do "ERC-721" => %{ "incoming" => watchlist.watch_erc_721_input, "outcoming" => watchlist.watch_erc_721_output + }, + "ERC-404" => %{ + "incoming" => watchlist.watch_erc_404_input, + "outcoming" => watchlist.watch_erc_404_output } } diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs index 2a05b0c65980..37f279b32562 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs @@ -7,6 +7,8 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do import Mox + setup :verify_on_exit! + describe "GET index/3" do setup :set_mox_global @@ -51,7 +53,7 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - get_eip1967_implementation() + request_zero_implementations() conn = get(conn, address_read_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) @@ -82,7 +84,7 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do end end - def get_eip1967_implementation do + def request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -120,5 +122,17 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs index 77d5a8060f9e..14b42c6ae55f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs @@ -7,6 +7,8 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do import Mox + setup :verify_on_exit! + describe "GET index/3" do setup :set_mox_global @@ -51,7 +53,7 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - get_eip1967_implementation() + request_zero_implementations() conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) @@ -80,7 +82,7 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do end end - def get_eip1967_implementation do + def request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -118,5 +120,17 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs index b96ede11e8db..475987d235a1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs @@ -161,6 +161,37 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do assert 1 = length(response_2nd_page["items"]) end + test "returns next page of results based on last seen token for erc-404", %{conn: conn} do + address = insert(:address) + + 1..51 + |> Enum.reduce([], fn _i, acc -> + token = insert(:token, name: "FN2 Token", type: "ERC-404") + + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + address: address, + value: 3 + ) + + acc ++ [token.name] + end) + + conn = + get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "type" => "JSON" + }) + + assert response = json_response(conn, 200) + + request_2nd_page = get(conn, response["next_page_path"], %{"type" => "JSON"}) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert 1 = length(response_2nd_page["items"]) + end + test "next_page_params exists if not on last page", %{conn: conn} do address = insert(:address) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs index b577be9c74e3..beb197b8b592 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs @@ -9,6 +9,8 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do import Mox + setup :verify_on_exit! + describe "GET index/3" do setup :set_mox_global @@ -53,7 +55,7 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - get_eip1967_implementation() + request_zero_implementations() conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) @@ -84,7 +86,7 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do end end - def get_eip1967_implementation do + def request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -122,5 +124,17 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs index 01b1b931134a..377f8f1a3881 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs @@ -7,6 +7,8 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do import Mox + setup :verify_on_exit! + describe "GET index/3" do setup :set_mox_global @@ -51,7 +53,7 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - get_eip1967_implementation() + request_zero_implementations() conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) @@ -82,7 +84,7 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do end end - def get_eip1967_implementation do + def request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -120,5 +122,17 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index ae0b2f3b944e..5a9475258e3f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -131,24 +131,24 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do mining_address = insert(:address, fetched_coin_balance: 0, - fetched_coin_balance_block_number: 102, + fetched_coin_balance_block_number: 103, inserted_at: Timex.shift(now, minutes: -10) ) mining_address_hash = to_string(mining_address.hash) # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far # back we'd need to go to get 24 hours in the past) - Enum.each(0..100, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, hours: -(102 - i) * 25), miner: mining_address) + Enum.each(0..101, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(103 - i) * 25), miner: mining_address) end) - insert(:block, number: 101, timestamp: Timex.shift(now, hours: -25), miner: mining_address) + insert(:block, number: 102, timestamp: Timex.shift(now, hours: -25), miner: mining_address) AverageBlockTime.refresh() address = insert(:address, fetched_coin_balance: 100, - fetched_coin_balance_block_number: 100, + fetched_coin_balance_block_number: 101, inserted_at: Timex.shift(now, minutes: -5) ) @@ -158,20 +158,20 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do %{ id: id, method: "eth_getBalance", - params: [^mining_address_hash, "0x65"] + params: [^mining_address_hash, "0x66"] } ], _options -> {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} end) - res = eth_block_number_fake_response("0x65") + res = eth_block_number_fake_response("0x66") expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ %{ id: 0, method: "eth_getBlockByNumber", - params: ["0x65", true] + params: ["0x66", true] } ], _ -> @@ -182,7 +182,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do %{ id: id, method: "eth_getBalance", - params: [^address_hash, "0x65"] + params: [^address_hash, "0x66"] } ], _options -> @@ -193,7 +193,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do %{ id: 0, method: "eth_getBlockByNumber", - params: ["0x65", true] + params: ["0x66", true] } ], _ -> @@ -229,7 +229,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert received_address.hash == address.hash assert received_address.fetched_coin_balance == expected_wei - assert received_address.fetched_coin_balance_block_number == 101 + assert received_address.fetched_coin_balance_block_number == 102 end end @@ -1090,7 +1090,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) end - test "with start_block and end_block params", %{conn: conn} do + test "with startblock and endblock params", %{conn: conn} do blocks = [_, second_block, third_block, _] = insert_list(4, :block) address = insert(:address) @@ -1104,8 +1104,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "module" => "account", "action" => "txlist", "address" => "#{address.hash}", - "start_block" => "#{second_block.number}", - "end_block" => "#{third_block.number}" + "startblock" => "#{second_block.number}", + "endblock" => "#{third_block.number}" } expected_block_numbers = [ @@ -1129,7 +1129,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) end - test "with start_block but without end_block", %{conn: conn} do + test "with startblock but without endblock", %{conn: conn} do blocks = [_, _, third_block, fourth_block] = insert_list(4, :block) address = insert(:address) @@ -1143,7 +1143,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "module" => "account", "action" => "txlist", "address" => "#{address.hash}", - "start_block" => "#{third_block.number}" + "startblock" => "#{third_block.number}" } expected_block_numbers = [ @@ -1167,7 +1167,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) end - test "with end_block but without start_block", %{conn: conn} do + test "with endblock but without startblock", %{conn: conn} do blocks = [first_block, second_block, _, _] = insert_list(4, :block) address = insert(:address) @@ -1181,7 +1181,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "module" => "account", "action" => "txlist", "address" => "#{address.hash}", - "end_block" => "#{second_block.number}" + "endblock" => "#{second_block.number}" } expected_block_numbers = [ @@ -1205,7 +1205,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) end - test "ignores invalid start_block and end_block", %{conn: conn} do + test "ignores invalid startblock and endblock", %{conn: conn} do blocks = [_, _, _, _] = insert_list(4, :block) address = insert(:address) @@ -1219,8 +1219,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "module" => "account", "action" => "txlist", "address" => "#{address.hash}", - "start_block" => "invalidstart", - "end_block" => "invalidend" + "startblock" => "invalidstart", + "endblock" => "invalidend" } assert response = @@ -1246,7 +1246,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do for block <- Enum.concat([blocks1, blocks2, blocks3]) do 2 - |> insert_list(:transaction, from_address: address) + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) |> with_block(block) end @@ -1294,7 +1294,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do for block <- Enum.concat([blocks1, blocks2, blocks3]) do 2 - |> insert_list(:transaction, from_address: address) + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) |> with_block(block) end @@ -1342,7 +1342,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do for block <- Enum.concat([blocks1, blocks2, blocks3]) do 2 - |> insert_list(:transaction, from_address: address) + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) |> with_block(block) end @@ -2368,6 +2368,284 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do end end + describe "tokennfttx" do + setup do + %{params: %{"module" => "account", "action" => "tokennfttx"}} + end + + test "with missing address and contract address hash", %{conn: conn, params: params} do + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "Query parameter address or contractaddress is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid address hash", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "badhash"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No token transfers found" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "has correct value for ERC-721", %{conn: conn, params: params} do + transaction = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + insert(:token, %{contract_address: token_address, type: "ERC-721"}) + + token_transfer = + insert(:token_transfer, %{ + token_contract_address: token_address, + token_ids: [666], + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + }) + + {:ok, _} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["tokenID"] == to_string(List.first(token_transfer.token_ids)) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "returns all the required fields", %{conn: conn, params: params} do + transaction = + %{block: block} = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + token = insert(:token, contract_address: token_address, type: "ERC-721") + + token_transfer = + insert(:token_transfer, + block: transaction.block, + transaction: transaction, + block_number: block.number, + token_ids: [1010], + token_contract_address: token_address + ) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + expected_result = [ + %{ + "blockNumber" => to_string(transaction.block_number), + "timeStamp" => to_string(DateTime.to_unix(block.timestamp)), + "hash" => to_string(token_transfer.transaction_hash), + "nonce" => to_string(transaction.nonce), + "blockHash" => to_string(block.hash), + "from" => to_string(token_transfer.from_address_hash), + "contractAddress" => to_string(token_transfer.token_contract_address_hash), + "to" => to_string(token_transfer.to_address_hash), + "tokenName" => token.name, + "tokenSymbol" => token.symbol, + "tokenDecimal" => to_string(token.decimals), + "transactionIndex" => to_string(transaction.index), + "gas" => to_string(transaction.gas), + "gasPrice" => to_string(transaction.gas_price.value), + "gasUsed" => to_string(transaction.gas_used), + "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), + "logIndex" => to_string(token_transfer.log_index), + "input" => "deprecated", + "confirmations" => "0", + "tokenID" => "1010" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid contract address", %{conn: conn, params: params} do + params = + Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", "contractaddress" => "invalid"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "filters results by contract address", %{conn: conn, params: params} do + address = insert(:address) + + contract_address = insert(:contract_address) + + insert(:token, contract_address: contract_address, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:token_transfer, + from_address: address, + token_contract_address: contract_address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [123] + ) + + params = + Map.merge(params, %{"address" => to_string(address.hash), "contractaddress" => to_string(contract_address.hash)}) + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["contractAddress"] == to_string(contract_address.hash) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "Check pagination and ordering (page, offset, sort parameters)", %{conn: conn, params: params} do + address = insert(:address) + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + tx = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + # sort: asc + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 1, "sort" => "asc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 2, "sort" => "asc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 3, "sort" => "asc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_721_tt, 0).transaction_hash) + assert Enum.at(token_transfers_1, 24)["hash"] == to_string(Enum.at(erc_721_tt, 24).transaction_hash) + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_721_tt, 25).transaction_hash) + assert Enum.at(token_transfers_2, 24)["hash"] == to_string(Enum.at(erc_721_tt, 49).transaction_hash) + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_721_tt, 50).transaction_hash) + assert Enum.count(token_transfers_3) == 1 + + # sort: desc + erc_721_tt_reversed = Enum.reverse(erc_721_tt) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 1, "sort" => "desc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 2, "sort" => "desc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 25, "page" => 3, "sort" => "desc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 0).transaction_hash) + assert Enum.at(token_transfers_1, 24)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 24).transaction_hash) + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 25).transaction_hash) + assert Enum.at(token_transfers_2, 24)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 49).transaction_hash) + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_721_tt_reversed, 50).transaction_hash) + assert Enum.count(token_transfers_3) == 1 + end + end + describe "tokenbalance" do test "without required params", %{conn: conn} do params = %{ @@ -2830,8 +3108,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do describe "optional_params/1" do test "includes valid optional params in the required format" do params = %{ - "start_block" => "100", - "end_block" => "120", + "startblock" => "100", + "endblock" => "120", "sort" => "asc", # page number "page" => "1", @@ -2850,8 +3128,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert optional_params.page_number == 1 assert optional_params.page_size == 2 assert optional_params.order_by_direction == :asc - assert optional_params.start_block == 100 - assert optional_params.end_block == 120 + assert optional_params.startblock == 100 + assert optional_params.endblock == 120 assert optional_params.filter_by == "to" assert optional_params.start_timestamp == expected_timestamp assert optional_params.end_timestamp == expected_timestamp @@ -2899,8 +3177,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do test "ignores invalid optional params, keeps valid ones" do params1 = %{ - "start_block" => "invalid", - "end_block" => "invalid", + "startblock" => "invalid", + "endblock" => "invalid", "sort" => "invalid", "page" => "invalid", "offset" => "invalid", @@ -2911,8 +3189,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert AddressController.optional_params(params1) == %{} params2 = %{ - "start_block" => "4", - "end_block" => "10", + "startblock" => "4", + "endblock" => "10", "sort" => "invalid", "page" => "invalid", "offset" => "invalid", @@ -2922,8 +3200,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do optional_params = AddressController.optional_params(params2) - assert optional_params.start_block == 4 - assert optional_params.end_block == 10 + assert optional_params.startblock == 4 + assert optional_params.endblock == 10 end test "ignores 'page' if less than 1" do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs index 70e9726fe7cc..ef8ce3513d47 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs @@ -1,8 +1,11 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do use BlockScoutWeb.ConnCase - alias Explorer.Chain.{Hash, Wei} + import EthereumJSONRPC, only: [integer_to_quantity: 1] + alias BlockScoutWeb.Chain + alias Explorer.Chain.{Hash, Wei} + alias Explorer.Counters.AverageBlockTime describe "getblockreward" do test "with missing block number", %{conn: conn} do @@ -15,7 +18,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -29,7 +32,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -43,7 +46,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -55,19 +58,103 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do |> insert(gas_price: 1) |> with_block(block, gas_used: 1) + block_quantity = integer_to_quantity(block.number) + expected_reward = emission_reward.reward |> Wei.to(:wei) |> Decimal.add(Decimal.new(1)) - |> Decimal.to_string(:normal) + + insert(:reward, address_hash: block.miner_hash, block_hash: block.hash, reward: expected_reward) + + expected_result = %{ + "blockNumber" => "#{block.number}", + "timeStamp" => DateTime.to_unix(block.timestamp), + "blockMiner" => Hash.to_string(block.miner_hash), + "blockReward" => expected_reward |> Decimal.to_string(:normal), + "uncles" => [], + "uncleInclusionReward" => "0" + } + + assert response = + conn + |> get("/api", %{"module" => "block", "action" => "getblockreward", "blockno" => "#{block.number}"}) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with a valid block and uncles", %{conn: conn} do + %{block_range: range} = emission_reward = insert(:emission_reward) + block = insert(:block, number: Enum.random(Range.new(range.from + 2, range.to))) + uncle1 = insert(:block, number: block.number - 1) + uncle2 = insert(:block, number: block.number - 2) + + insert(:block_second_degree_relation, nephew: block, uncle_hash: uncle1.hash, index: 0) + insert(:block_second_degree_relation, nephew: block, uncle_hash: uncle2.hash, index: 1) + + :transaction + |> insert(gas_price: 1) + |> with_block(block, gas_used: 1) + + block_quantity = integer_to_quantity(block.number) + + decimal_emission_reward = Wei.to(emission_reward.reward, :wei) + + uncle1_reward = + decimal_emission_reward |> Decimal.div(8) |> Decimal.mult(Decimal.new(uncle1.number + 8 - block.number)) + + uncle2_reward = + decimal_emission_reward |> Decimal.div(8) |> Decimal.mult(Decimal.new(uncle2.number + 8 - block.number)) + + uncle_inclusion_reward = + decimal_emission_reward + |> Decimal.div(Decimal.new(32)) + |> Decimal.mult(Decimal.new(2)) + + block_reward = + decimal_emission_reward + |> Decimal.add(Decimal.new(1)) + |> Decimal.add(uncle_inclusion_reward) + + insert(:reward, address_hash: block.miner_hash, block_hash: block.hash, reward: block_reward) + + insert(:reward, + address_hash: uncle1.miner_hash, + block_hash: block.hash, + reward: uncle1_reward, + address_type: :uncle + ) + + insert(:reward, + address_hash: uncle2.miner_hash, + block_hash: block.hash, + reward: uncle2_reward, + address_type: :uncle + ) expected_result = %{ "blockNumber" => "#{block.number}", "timeStamp" => DateTime.to_unix(block.timestamp), "blockMiner" => Hash.to_string(block.miner_hash), - "blockReward" => expected_reward, - "uncles" => nil, - "uncleInclusionReward" => nil + "blockReward" => block_reward |> Decimal.to_string(:normal), + "uncles" => [ + %{ + "blockreward" => uncle1_reward |> Decimal.to_string(:normal), + "miner" => uncle1.miner_hash |> Hash.to_string(), + "unclePosition" => "0" + }, + %{ + "blockreward" => uncle2_reward |> Decimal.to_string(:normal), + "miner" => uncle2.miner_hash |> Hash.to_string(), + "unclePosition" => "1" + } + ], + "uncleInclusionReward" => "0" } assert response = @@ -78,7 +165,57 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["result"] == expected_result assert response["status"] == "1" assert response["message"] == "OK" - schema = resolve_schema() + schema = resolve_getblockreward_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + end + + describe "getblockcountdown" do + setup do + start_supervised!(AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + end + + test "returns countdown information when valid block number is provided", %{conn: conn} do + unsafe_target_block_number = "120" + current_block_number = 110 + average_block_time = 15 + remaining_blocks = 10 + + first_timestamp = Timex.now() + + for i <- 1..current_block_number do + insert(:block, number: i, timestamp: Timex.shift(first_timestamp, seconds: i * average_block_time)) + end + + AverageBlockTime.refresh() + + estimated_time_in_sec = Float.round(remaining_blocks * average_block_time * 1.0, 1) + + expected_result = %{ + "CurrentBlock" => "#{current_block_number}", + "CountdownBlock" => unsafe_target_block_number, + "RemainingBlock" => "#{remaining_blocks}", + "EstimateTimeInSec" => "#{estimated_time_in_sec}" + } + + response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblockcountdown", + "blockno" => unsafe_target_block_number + }) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + schema = resolve_getblockcountdown_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end end @@ -94,7 +231,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -108,7 +245,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -127,7 +264,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -146,7 +283,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -178,7 +315,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["result"] == expected_result assert response["status"] == "1" assert response["message"] == "OK" - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -210,12 +347,12 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["result"] == expected_result assert response["status"] == "1" assert response["message"] == "OK" - schema = resolve_schema() + schema = resolve_getblockreward_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end end - defp resolve_schema() do + defp resolve_getblockreward_schema() do ExJsonSchema.Schema.resolve(%{ "type" => "object", "properties" => %{ @@ -228,8 +365,37 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do "timeStamp" => %{"type" => "number"}, "blockMiner" => %{"type" => "string"}, "blockReward" => %{"type" => "string"}, - "uncles" => %{"type" => "null"}, - "uncleInclusionReward" => %{"type" => "null"} + "uncles" => %{ + "type" => "array", + "items" => %{ + "type" => "object", + "properties" => %{ + "miner" => %{"type" => "string"}, + "unclePosition" => %{"type" => "string"}, + "blockreward" => %{"type" => "string"} + } + } + }, + "uncleInclusionReward" => %{"type" => "string"} + } + } + } + }) + end + + defp resolve_getblockcountdown_schema() do + ExJsonSchema.Schema.resolve(%{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"}, + "result" => %{ + "type" => "object", + "properties" => %{ + "CurrentBlock" => %{"type" => "string"}, + "CountdownBlock" => %{"type" => "string"}, + "RemainingBlock" => %{"type" => "string"}, + "EstimateTimeInSec" => %{"type" => "string"} } } } diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 752c393c4710..f03df2b1932a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -1,11 +1,11 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do use BlockScoutWeb.ConnCase alias Explorer.Chain.SmartContract - alias Explorer.Chain - # alias Explorer.{Chain, Factory} import Mox + setup :verify_on_exit! + def prepare_contracts do insert(:contract_address) {:ok, dt_1, _} = DateTime.from_iso8601("2022-09-20 10:00:00Z") @@ -649,7 +649,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do %SmartContract.ExternalLibrary{:address_hash => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f", :name => "Test2"} ] - {:ok, %SmartContract{} = contract} = Chain.create_smart_contract(valid_attrs, external_libraries) + {:ok, %SmartContract{} = contract} = SmartContract.create_smart_contract(valid_attrs, external_libraries) params = %{ "module" => "contract", @@ -771,7 +771,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do # |> get("/api", params) # |> json_response(200) - # verified_contract = Chain.address_hash_to_smart_contract(contract_address.hash) + # verified_contract = SmartContract.address_hash_to_smart_contract(contract_address.hash) # expected_result = %{ # "Address" => to_string(contract_address.hash), @@ -844,7 +844,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do # result = response["result"] - # verified_contract = Chain.address_hash_to_smart_contract(contract_address.hash) + # verified_contract = SmartContract.address_hash_to_smart_contract(contract_address.hash) # assert result["Address"] == to_string(contract_address.hash) @@ -860,6 +860,63 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do # end end + describe "getcontractcreation" do + setup do + %{params: %{"module" => "contract", "action" => "getcontractcreation"}} + end + + test "return error", %{conn: conn, params: params} do + %{ + "status" => "0", + "message" => "Query parameter contractaddresses is required", + "result" => "Query parameter contractaddresses is required" + } = + conn + |> get("/api", params) + |> json_response(200) + end + + test "get empty list", %{conn: conn, params: params} do + address = build(:address) + address_1 = insert(:address) + + %{ + "status" => "1", + "message" => "OK", + "result" => [] + } = + conn + |> get("/api", Map.put(params, "contractaddresses", "#{to_string(address)},#{to_string(address_1)}")) + |> json_response(200) + end + + test "get not empty list", %{conn: conn, params: params} do + address_1 = build(:address) + address = insert(:contract_address) + + transaction = insert(:transaction, created_contract_address: address) + + %{ + "status" => "1", + "message" => "OK", + "result" => [ + %{ + "contractAddress" => contract_address, + "contractCreator" => contract_creator, + "txHash" => tx_hash + } + ] + } = + conn + |> get("/api", Map.put(params, "contractaddresses", "#{to_string(address)},#{to_string(address_1)}")) + |> json_response(200) + + assert contract_address == to_string(address.hash) + assert contract_creator == to_string(transaction.from_address_hash) + assert tx_hash == to_string(transaction.hash) + end + end + defp listcontracts_schema do resolve_schema(%{ "type" => ["array", "null"], @@ -967,5 +1024,17 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs index e80a8bbc7c92..5b4bb4f6fc79 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs @@ -5,6 +5,12 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do alias Explorer.Repo alias Indexer.Fetcher.CoinBalanceOnDemand + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @first_topic_hex_string_2 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + @second_topic_hex_string_2 "0x000000000000000000000000e2680fd7cdbb04e9087a647ad4d023ef6c8fb4e2" + setup do mocked_json_rpc_named_arguments = [ transport: EthereumJSONRPC.Mox, @@ -27,6 +33,11 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do defp params(api_params, params), do: Map.put(api_params, "params", params) + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + describe "eth_get_logs" do setup do %{ @@ -76,7 +87,14 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do block = insert(:block, number: 0) transaction = insert(:transaction, from_address: address) |> with_block(block) - insert(:log, block: block, address: address, transaction: transaction, data: "0x010101") + + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x010101" + ) params = params(api_params, [%{"address" => to_string(address.hash)}]) @@ -94,9 +112,17 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do block = insert(:block, number: 0) transaction = insert(:transaction, from_address: address) |> with_block(block) - insert(:log, block: block, address: address, transaction: transaction, data: "0x010101", first_topic: "0x01") - params = params(api_params, [%{"address" => to_string(address.hash), "topics" => ["0x01"]}]) + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1) + ) + + params = params(api_params, [%{"address" => to_string(address.hash), "topics" => [@first_topic_hex_string_1]}]) assert response = conn @@ -112,10 +138,29 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do block = insert(:block, number: 0) transaction = insert(:transaction, from_address: address) |> with_block(block) - insert(:log, address: address, block: block, transaction: transaction, data: "0x010101", first_topic: "0x01") - insert(:log, address: address, block: block, transaction: transaction, data: "0x020202", first_topic: "0x00") - params = params(api_params, [%{"address" => to_string(address.hash), "topics" => [["0x01", "0x00"]]}]) + insert(:log, + address: address, + block: block, + block_number: block.number, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1) + ) + + insert(:log, + address: address, + block: block, + block_number: block.number, + transaction: transaction, + data: "0x020202", + first_topic: topic(@first_topic_hex_string_2) + ) + + params = + params(api_params, [ + %{"address" => to_string(address.hash), "topics" => [[@first_topic_hex_string_1, @first_topic_hex_string_2]]} + ]) assert response = conn @@ -135,9 +180,16 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do |> with_block(block) inserted_records = - insert_list(2000, :log, block: block, address: contract_address, transaction: transaction, first_topic: "0x01") + insert_list(2000, :log, + block: block, + block_number: block.number, + address: contract_address, + transaction: transaction, + first_topic: topic(@first_topic_hex_string_1) + ) - params = params(api_params, [%{"address" => to_string(contract_address), "topics" => [["0x01"]]}]) + params = + params(api_params, [%{"address" => to_string(contract_address), "topics" => [[@first_topic_hex_string_1]]}]) assert response = conn @@ -146,17 +198,21 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do assert Enum.count(response["result"]) == 1000 - {last_log_index, ""} = Integer.parse(List.last(response["result"])["logIndex"], 16) + "0x" <> hexadecimal_digits = List.last(response["result"])["logIndex"] + {last_log_index, ""} = Integer.parse(hexadecimal_digits, 16) next_page_params = %{ "blockNumber" => Integer.to_string(transaction.block_number, 16), - "transactionIndex" => transaction.index, "logIndex" => Integer.to_string(last_log_index, 16) } new_params = params(api_params, [ - %{"paging_options" => next_page_params, "address" => to_string(contract_address), "topics" => [["0x01"]]} + %{ + "paging_options" => next_page_params, + "address" => to_string(contract_address), + "topics" => [[@first_topic_hex_string_1]] + } ]) assert new_response = @@ -170,7 +226,8 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do assert Enum.all?(inserted_records, fn record -> Enum.any?(all_found_logs, fn found_log -> - {index, ""} = Integer.parse(found_log["logIndex"], 16) + "0x" <> hexadecimal_digits = found_log["logIndex"] + {index, ""} = Integer.parse(hexadecimal_digits, 16) record.index == index end) @@ -191,14 +248,24 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do address: address, transaction: transaction, data: "0x010101", - first_topic: "0x01", - second_topic: "0x02", - block: block + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block: block, + block_number: block.number ) - insert(:log, block: block, address: address, transaction: transaction, data: "0x020202", first_topic: "0x01") + insert(:log, + block: block, + address: address, + transaction: transaction, + data: "0x020202", + first_topic: topic(@first_topic_hex_string_1) + ) - params = params(api_params, [%{"address" => to_string(address.hash), "topics" => ["0x01", "0x02"]}]) + params = + params(api_params, [ + %{"address" => to_string(address.hash), "topics" => [@first_topic_hex_string_1, @second_topic_hex_string_1]} + ]) assert response = conn @@ -220,21 +287,29 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do address: address, transaction: transaction, data: "0x010101", - first_topic: "0x01", - second_topic: "0x02", - block: block + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block: block, + block_number: block.number ) insert(:log, address: address, transaction: transaction, data: "0x020202", - first_topic: "0x01", - second_topic: "0x03", - block: block + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_2), + block: block, + block_number: block.number ) - params = params(api_params, [%{"address" => to_string(address.hash), "topics" => ["0x01", ["0x02", "0x03"]]}]) + params = + params(api_params, [ + %{ + "address" => to_string(address.hash), + "topics" => [@first_topic_hex_string_1, [@second_topic_hex_string_1, @second_topic_hex_string_2]] + } + ]) assert response = conn @@ -258,13 +333,13 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do transaction3 = insert(:transaction, from_address: address) |> with_block(block3) transaction4 = insert(:transaction, from_address: address) |> with_block(block4) - insert(:log, address: address, transaction: transaction1, data: "0x010101") + insert(:log, address: address, transaction: transaction1, data: "0x010101", block_number: block1.number) - insert(:log, address: address, transaction: transaction2, data: "0x020202") + insert(:log, address: address, transaction: transaction2, data: "0x020202", block_number: block2.number) - insert(:log, address: address, transaction: transaction3, data: "0x030303") + insert(:log, address: address, transaction: transaction3, data: "0x030303", block_number: block3.number) - insert(:log, address: address, transaction: transaction4, data: "0x040404") + insert(:log, address: address, transaction: transaction4, data: "0x040404", block_number: block4.number) params = params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => 1, "toBlock" => 2}]) @@ -288,11 +363,11 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do transaction2 = insert(:transaction, from_address: address) |> with_block(block2) transaction3 = insert(:transaction, from_address: address) |> with_block(block3) - insert(:log, address: address, transaction: transaction1, data: "0x010101") + insert(:log, address: address, transaction: transaction1, data: "0x010101", block_number: block1.number) - insert(:log, address: address, transaction: transaction2, data: "0x020202") + insert(:log, address: address, transaction: transaction2, data: "0x020202", block_number: block2.number) - insert(:log, address: address, transaction: transaction3, data: "0x030303") + insert(:log, address: address, transaction: transaction3, data: "0x030303", block_number: block3.number) params = params(api_params, [%{"address" => to_string(address.hash), "blockHash" => to_string(block2.hash)}]) @@ -316,11 +391,11 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do transaction2 = insert(:transaction, from_address: address) |> with_block(block2) transaction3 = insert(:transaction, from_address: address) |> with_block(block3) - insert(:log, address: address, transaction: transaction1, data: "0x010101") + insert(:log, address: address, transaction: transaction1, data: "0x010101", block_number: block1.number) - insert(:log, address: address, transaction: transaction2, data: "0x020202") + insert(:log, address: address, transaction: transaction2, data: "0x020202", block_number: block2.number) - insert(:log, address: address, transaction: transaction3, data: "0x030303") + insert(:log, address: address, transaction: transaction3, data: "0x030303", block_number: block3.number) params = params(api_params, [%{"address" => to_string(address.hash), "fromBlock" => "earliest", "toBlock" => "earliest"}]) @@ -345,11 +420,29 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do transaction2 = insert(:transaction, from_address: address) |> with_block(block2) transaction3 = insert(:transaction, from_address: address) |> with_block(block3) - insert(:log, block: block1, address: address, transaction: transaction1, data: "0x010101") + insert(:log, + block: block1, + block_number: block1.number, + address: address, + transaction: transaction1, + data: "0x010101" + ) - insert(:log, block: block2, address: address, transaction: transaction2, data: "0x020202") + insert(:log, + block: block2, + block_number: block2.number, + address: address, + transaction: transaction2, + data: "0x020202" + ) - insert(:log, block: block3, address: address, transaction: transaction3, data: "0x030303") + insert(:log, + block: block3, + block_number: block3.number, + address: address, + transaction: transaction3, + data: "0x030303" + ) changeset = Ecto.Changeset.change(block3, %{consensus: false}) @@ -365,6 +458,49 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do assert [%{"data" => "0x030303"}] = response["result"] end + + test "numerical fields are hexadecimals with 0x prefix", + %{conn: conn, api_params: api_params} do + address = insert(:address) + block = insert(:block, number: 0) + transaction = insert(:transaction, from_address: address) |> with_block(block) + + insert(:log, + block: block, + block_number: block.number, + address: address, + transaction: transaction, + data: "0x010101", + first_topic: topic(@first_topic_hex_string_1) + ) + + params = + params(api_params, [ + %{ + "address" => to_string(address.hash), + "topics" => [@first_topic_hex_string_1] + } + ]) + + response = + conn + |> post("/api/eth-rpc", params) + |> json_response(200) + + [result] = response["result"] + + assert result + |> Map.take([ + "address", + "blockHash", + "blockNumber", + "data", + "transactionIndex", + "logIndex", + "transactionHash" + ]) + |> Enum.all?(fn {_, v} -> String.starts_with?(v, "0x") end) + end end describe "eth_get_balance" do @@ -400,9 +536,7 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do test "with a valid address that has a balance", %{conn: conn, api_params: api_params} do block = insert(:block) - address = insert(:address) - - insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: block.number) assert response = conn diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs index ec4337eecd84..2691f1e7e825 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/logs_controller_test.exs @@ -4,6 +4,22 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do alias BlockScoutWeb.API.RPC.LogsController alias Explorer.Chain.{Log, Transaction} + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @first_topic_hex_string_2 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + @second_topic_hex_string_2 "0x000000000000000000000000e2680fd7cdbb04e9087a647ad4d023ef6c8fb4e2" + + @third_topic_hex_string_1 "0x0000000000000000000000005079fc00f00f30000e0c8c083801cfde000008b6" + + @fourth_topic_hex_string_1 "0x8c9b7729443a4444242342b2ca385a239a5c1d76a88473e1cd2ab0c70dd1b9c7" + @fourth_topic_hex_string_2 "0x232b688786cc0d24a11e07563c1bfa129537cec9385dc5b1fb8f86462977239b" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + describe "getLogs" do test "without fromBlock, toBlock, address, and topic{x}", %{conn: conn} do params = %{ @@ -280,7 +296,7 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do |> insert(to_address: contract_address) |> with_block() - log = insert(:log, address: contract_address, transaction: transaction) + log = insert(:log, address: contract_address, transaction: transaction, block_number: transaction.block_number) params = %{ "module" => "logs", @@ -334,8 +350,17 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do |> insert(to_address: contract_address) |> with_block(second_block) - insert(:log, address: contract_address, transaction: transaction_block1) - insert(:log, address: contract_address, transaction: transaction_block2) + insert(:log, + address: contract_address, + transaction: transaction_block1, + block_number: transaction_block1.block_number + ) + + insert(:log, + address: contract_address, + transaction: transaction_block2, + block_number: transaction_block2.block_number + ) params = %{ "module" => "logs", @@ -378,8 +403,17 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do |> insert(to_address: contract_address) |> with_block(second_block) - insert(:log, address: contract_address, transaction: transaction_block1) - insert(:log, address: contract_address, transaction: transaction_block2) + insert(:log, + address: contract_address, + transaction: transaction_block1, + block_number: transaction_block1.block_number + ) + + insert(:log, + address: contract_address, + transaction: transaction_block2, + block_number: transaction_block2.block_number + ) params = %{ "module" => "logs", @@ -416,13 +450,13 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic" + first_topic: topic(@first_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some other topic" + first_topic: topic(@first_topic_hex_string_2) ] log1 = insert(:log, log1_details) @@ -474,15 +508,15 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some other topic", - second_topic: "some other second topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2) ] log1 = insert(:log, log1_details) @@ -523,15 +557,15 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some other topic", - second_topic: "some other second topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2) ] log1 = insert(:log, log1_details) @@ -571,19 +605,19 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic", - third_topic: "some third topic", - fourth_topic: "some fourth topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + fourth_topic: topic(@fourth_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic", - third_topic: "some third topic", - fourth_topic: "some other fourth topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + fourth_topic: topic(@fourth_topic_hex_string_2) ] log1 = insert(:log, log1_details) @@ -773,7 +807,12 @@ defmodule BlockScoutWeb.API.RPC.LogsControllerTest do third_topic: third_topic, fourth_topic: fourth_topic }) do - [first_topic, second_topic, third_topic, fourth_topic] + [ + first_topic && Explorer.Chain.Hash.to_string(first_topic), + second_topic && Explorer.Chain.Hash.to_string(second_topic), + third_topic && Explorer.Chain.Hash.to_string(third_topic), + fourth_topic && Explorer.Chain.Hash.to_string(fourth_topic) + ] end defp integer_to_hex(integer), do: "0x" <> String.downcase(Integer.to_string(integer, 16)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs index a5b23b25323b..bf410d62a14a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/rpc_translator_test.exs @@ -28,7 +28,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslatorTest do result = RPCTranslator.call(conn, %{}) assert result.halted assert response = json_response(result, 400) - assert response["message"] =~ "Unknown action" + assert response["message"] =~ "Unknown module" assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] @@ -78,5 +78,12 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslatorTest do result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) assert json_response(result, 200) == %{} end + + test "allow multiple '/' before api", %{conn: conn} do + conn = %Conn{conn | params: %{"module" => "test", "action" => "test_action"}, request_path: "//api"} + + result = RPCTranslator.call(conn, %{"test" => {TestController, []}}) + assert json_response(result, 200) == %{} + end end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs index 5731311f11b7..a883a8d96e16 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs @@ -173,7 +173,8 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do name: "test", symbol: symbol, usd_value: Decimal.new("1.0"), - volume_24h_usd: Decimal.new("1000.0") + volume_24h_usd: Decimal.new("1000.0"), + image_url: nil } ExchangeRates.handle_info({nil, {:ok, [eth]}}, %{}) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs index e9e08b6a7127..8e30a3e68046 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -5,6 +5,16 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do @moduletag capture_log: true + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + + setup :verify_on_exit! + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + describe "gettxreceiptstatus" do test "with missing txhash", %{conn: conn} do params = %{ @@ -412,8 +422,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do insert(:log, address: address, transaction: transaction, - first_topic: "first topic", - second_topic: "second topic", + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), block: block, block_number: block.number ) @@ -489,8 +499,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do insert(:log, address: address, transaction: transaction, - first_topic: "first topic", - second_topic: "second topic", + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), block: block, block_number: block.number ) @@ -518,7 +528,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do %{ "address" => "#{address.hash}", "data" => "#{log.data}", - "topics" => ["first topic", "second topic", nil, nil], + "topics" => [@first_topic_hex_string_1, @second_topic_hex_string_1, nil, nil], "index" => "#{log.index}" } ], diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 962be40cafc0..15fe4de50b7f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do use BlockScoutWeb.ConnCase + use EthereumJSONRPC.Case, async: false + use BlockScoutWeb.ChannelCase + alias ABI.{TypeDecoder, TypeEncoder} alias BlockScoutWeb.Models.UserFromAuth alias Explorer.{Chain, Repo} alias Explorer.Chain.Address.Counters @@ -12,8 +15,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do InternalTransaction, Log, Token, + Token.Instance, TokenTransfer, Transaction, + Wei, Withdrawal } @@ -21,6 +26,19 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do alias Explorer.Chain.Address.CurrentTokenBalance import Explorer.Chain, only: [hash_to_lower_case_string: 1] + import Mox + + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @instances_amount_in_collection 9 + + setup :set_mox_global + + setup :verify_on_exit! + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end describe "/addresses/{address_hash}" do test "get 404 on non existing address", %{conn: conn} do @@ -43,7 +61,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do correct_response = %{ "hash" => Address.checksum(address.hash), "is_contract" => false, - "is_verified" => false, + "is_verified" => nil, "name" => nil, "private_tags" => [], "public_tags" => [], @@ -68,7 +86,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "has_tokens" => false, "has_token_transfers" => false, "watchlist_address_id" => nil, - "has_beacon_chain_withdrawals" => false + "has_beacon_chain_withdrawals" => false, + "ens_domain_name" => nil } request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") @@ -78,6 +97,47 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert ^correct_response = json_response(request, 200) end + test "get contract info", %{conn: conn} do + smart_contract = insert(:smart_contract) + + tx = + insert(:transaction, + to_address_hash: nil, + to_address: nil, + created_contract_address_hash: smart_contract.address_hash, + created_contract_address: smart_contract.address + ) + + insert(:address_name, + address: smart_contract.address, + primary: true, + name: smart_contract.name, + address_hash: smart_contract.address_hash + ) + + name = smart_contract.name + from = Address.checksum(tx.from_address_hash) + tx_hash = to_string(tx.hash) + address_hash = Address.checksum(smart_contract.address_hash) + + get_eip1967_implementation_non_zero_address() + + request = get(conn, "/api/v2/addresses/#{Address.checksum(smart_contract.address_hash)}") + + assert %{ + "hash" => ^address_hash, + "is_contract" => true, + "is_verified" => true, + "name" => ^name, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => ^from, + "creation_tx_hash" => ^tx_hash, + "implementation_address" => "0x0000000000000000000000000000000000000001" + } = json_response(request, 200) + end + test "get watchlist id", %{conn: conn} do auth = build(:auth) address = insert(:address) @@ -374,6 +434,217 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do check_paginated_response(response_2nd_page, response, txs_from ++ [Enum.at(txs_to, 0)]) end + + test "ignores wrong ordering params", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, from_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "foo", "order" => "bar"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "foo", "order" => "bar"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test "backward compatible with legacy paging params", %{conn: conn} do + address = insert(:address) + block = insert(:block) + + txs = insert_list(51, :transaction, from_address: address) |> with_block(block) + + [_, tx_before_last | _] = txs + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"block_number" => to_string(block.number), "index" => to_string(tx_before_last.index)} + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test "backward compatible with legacy paging params for pending transactions", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, from_address: address) + + [_, tx_before_last | _] = txs + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page_pending = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"inserted_at" => to_string(tx_before_last.inserted_at), "hash" => to_string(tx_before_last.hash)} + ) + + assert response_2nd_page_pending = json_response(request_2nd_page_pending, 200) + + check_paginated_response(response, response_2nd_page_pending, txs) + end + + test "can order and paginate by fee ascending", %{conn: conn} do + address = insert(:address) + + txs_from = insert_list(25, :transaction, from_address: address) |> with_block() + txs_to = insert_list(26, :transaction, to_address: address) |> with_block() + + txs = + (txs_from ++ txs_to) + |> Enum.sort( + &(Decimal.compare(&1 |> Transaction.fee(:wei) |> elem(1), &2 |> Transaction.fee(:wei) |> elem(1)) in [ + :eq, + :lt + ]) + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "fee", "order" => "asc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "fee", "order" => "asc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(txs, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(txs, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, txs |> Enum.reverse()) + end + + test "can order and paginate by fee descending", %{conn: conn} do + address = insert(:address) + + txs_from = insert_list(25, :transaction, from_address: address) |> with_block() + txs_to = insert_list(26, :transaction, to_address: address) |> with_block() + + txs = + (txs_from ++ txs_to) + |> Enum.sort( + &(Decimal.compare(&1 |> Transaction.fee(:wei) |> elem(1), &2 |> Transaction.fee(:wei) |> elem(1)) in [ + :eq, + :gt + ]) + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "fee", "order" => "desc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "fee", "order" => "desc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(txs, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(txs, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, txs |> Enum.reverse()) + end + + test "can order and paginate by value ascending", %{conn: conn} do + address = insert(:address) + + txs_from = insert_list(25, :transaction, from_address: address) |> with_block() + txs_to = insert_list(26, :transaction, to_address: address) |> with_block() + + txs = + (txs_from ++ txs_to) + |> Enum.sort(&(Decimal.compare(Wei.to(&1.value, :wei), Wei.to(&2.value, :wei)) in [:eq, :lt])) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "value", "order" => "asc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "value", "order" => "asc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(txs, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(txs, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, txs |> Enum.reverse()) + end + + test "can order and paginate by value descending", %{conn: conn} do + address = insert(:address) + + txs_from = insert_list(25, :transaction, from_address: address) |> with_block() + txs_to = insert_list(26, :transaction, to_address: address) |> with_block() + + txs = + (txs_from ++ txs_to) + |> Enum.sort(&(Decimal.compare(Wei.to(&1.value, :wei), Wei.to(&2.value, :wei)) in [:eq, :gt])) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "value", "order" => "desc"}) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/transactions", + %{"sort" => "value", "order" => "desc"} |> Map.merge(response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + compare_item(Enum.at(txs, 0), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs, 49), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + compare_item(Enum.at(txs, 50), Enum.at(response_2nd_page["items"], 0)) + + check_paginated_response(response, response_2nd_page, txs |> Enum.reverse()) + end end describe "/addresses/{address_hash}/token-transfers" do @@ -692,7 +963,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block: tx.block, block_number: tx.block_number, from_address: address, - token_contract_address: erc_20_token.contract_address + token_contract_address: erc_20_token.contract_address, + token_type: "ERC-20" ) end @@ -708,7 +980,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx.block_number, from_address: address, token_contract_address: erc_721_token.contract_address, - token_ids: [x] + token_ids: [x], + token_type: "ERC-721" ) end @@ -724,7 +997,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx.block_number, from_address: address, token_contract_address: erc_1155_token.contract_address, - token_ids: [x] + token_ids: [x], + token_type: "ERC-1155" ) end @@ -817,7 +1091,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block: tx.block, block_number: tx.block_number, from_address: address, - token_contract_address: erc_20_token.contract_address + token_contract_address: erc_20_token.contract_address, + token_type: "ERC-20" ) end @@ -833,7 +1108,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx.block_number, to_address: address, token_contract_address: erc_721_token.contract_address, - token_ids: [x] + token_ids: [x], + token_type: "ERC-721" ) end @@ -897,6 +1173,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) end @@ -931,7 +1208,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block: tx.block, block_number: tx.block_number, token_contract_address: token.contract_address, - token_ids: [i] + token_ids: [i], + token_type: "ERC-721" ) end @@ -959,6 +1237,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) @@ -1001,6 +1280,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx_1.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..24, fn x -> x end) ) @@ -1019,6 +1299,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx_2.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(25..49, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(25..49, fn x -> x end) ) @@ -1035,6 +1316,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx_2.block_number, token_contract_address: token.contract_address, token_ids: [50], + token_type: "ERC-1155", amounts: [50] ) @@ -1063,6 +1345,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx_1.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..24, fn x -> x end) ) @@ -1081,6 +1364,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: tx_2.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(25..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(25..50, fn x -> x end) ) @@ -1504,10 +1788,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block: tx.block, block_number: tx.block_number, address: address, - first_topic: "0x123456789123456789" + first_topic: topic(@first_topic_hex_string_1) ) - request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=0x123456789123456789") + request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=#{@first_topic_hex_string_1}") assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil @@ -1553,7 +1837,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do ) |> Repo.preload([:token]) end - |> Enum.sort_by(fn x -> x.value end, :asc) + |> Enum.sort_by(fn x -> Decimal.to_integer(x.value) end, :asc) ctbs_erc_1155 = for _ <- 0..50 do @@ -1564,7 +1848,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do ) |> Repo.preload([:token]) end - |> Enum.sort_by(fn x -> x.value end, :asc) + |> Enum.sort_by(fn x -> Decimal.to_integer(x.value) end, :asc) filter = %{"type" => "ERC-20"} request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) @@ -1601,6 +1885,291 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end end + describe "checks TokenBalanceOnDemand" do + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + old_env = Application.get_env(:indexer, Indexer.Fetcher.TokenBalanceOnDemand) + + Application.put_env( + :indexer, + Indexer.Fetcher.TokenBalanceOnDemand, + Keyword.put(old_env, :fallback_threshold_in_blocks, 0) + ) + + on_exit(fn -> + Application.put_env(:indexer, Indexer.Fetcher.TokenBalanceOnDemand, old_env) + end) + end + + test "Indexer.Fetcher.TokenBalanceOnDemand broadcasts only updated balances", %{conn: conn} do + address = insert(:address) + + ctbs_erc_20 = + for i <- 0..1 do + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-20", + token_id: nil + ) + + {to_string(ctb.token_contract_address_hash), + Decimal.to_integer(ctb.value) + if(rem(i, 2) == 0, do: 1, else: 0)} + end + |> Enum.into(%{}) + + ctbs_erc_721 = + for i <- 0..1 do + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-721", + token_id: nil + ) + + {to_string(ctb.token_contract_address_hash), + Decimal.to_integer(ctb.value) + if(rem(i, 2) == 0, do: 1, else: 0)} + end + |> Enum.into(%{}) + + other_balances = Map.merge(ctbs_erc_20, ctbs_erc_721) + + balances_erc_1155 = + for i <- 0..1 do + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + + {{to_string(ctb.token_contract_address_hash), to_string(ctb.token_id)}, + Decimal.to_integer(ctb.value) + if(rem(i, 2) == 0, do: 1, else: 0)} + end + |> Enum.into(%{}) + + block_number_hex = "0x" <> (Integer.to_string(insert(:block).number, 16) |> String.upcase()) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x00fdd58e" <> request_1, + to: contract_address_1 + }, + ^block_number_hex + ] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x00fdd58e" <> request_2, + to: contract_address_2 + }, + ^block_number_hex + ] + } + ], + _options -> + types_list = [:address, {:uint, 256}] + + [address_1, token_id_1] = request_1 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) + + assert address_1 == address.hash.bytes + + result_1 = + balances_erc_1155[{contract_address_1 |> String.downcase(), to_string(token_id_1)}] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + [address_2, token_id_2] = request_2 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) + + assert address_2 == address.hash.bytes + + result_2 = + balances_erc_1155[{contract_address_2 |> String.downcase(), to_string(token_id_2)}] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_1 + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_2 + } + ]} + end) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_1, + to: contract_address_1 + }, + ^block_number_hex + ] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_2, + to: contract_address_2 + }, + ^block_number_hex + ] + }, + %{ + id: id_3, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_3, + to: contract_address_3 + }, + ^block_number_hex + ] + }, + %{ + id: id_4, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x70a08231" <> request_4, + to: contract_address_4 + }, + ^block_number_hex + ] + } + ], + _options -> + types_list = [:address] + + assert request_1 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + assert request_2 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + assert request_3 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + assert request_4 |> Base.decode16!(case: :lower) |> TypeDecoder.decode_raw(types_list) == [address.hash.bytes] + + result_1 = + other_balances[contract_address_1 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + result_2 = + other_balances[contract_address_2 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + result_3 = + other_balances[contract_address_3 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + result_4 = + other_balances[contract_address_4 |> String.downcase()] + |> List.wrap() + |> TypeEncoder.encode_raw([{:uint, 256}], :standard) + |> Base.encode16(case: :lower) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_1 + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_2 + }, + %{ + id: id_3, + jsonrpc: "2.0", + result: "0x" <> result_3 + }, + %{ + id: id_4, + jsonrpc: "2.0", + result: "0x" <> result_4 + } + ]} + end) + + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.UserSocketV2 + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens") + assert _response = json_response(request, 200) + overflow = false + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_balances: [ctb_erc_20], overflow: ^overflow}, + event: "updated_token_balances_erc_20", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_balances: [ctb_erc_721], overflow: ^overflow}, + event: "updated_token_balances_erc_721", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_balances: [ctb_erc_1155], overflow: ^overflow}, + event: "updated_token_balances_erc_1155", + topic: ^topic + }, + :timer.seconds(1) + + assert Decimal.to_integer(ctb_erc_20["value"]) == + other_balances[ctb_erc_20["token"]["address"] |> String.downcase()] + + assert Decimal.to_integer(ctb_erc_721["value"]) == + other_balances[ctb_erc_721["token"]["address"] |> String.downcase()] + + assert Decimal.to_integer(ctb_erc_1155["value"]) == + balances_erc_1155[ + {ctb_erc_1155["token"]["address"] |> String.downcase(), to_string(ctb_erc_1155["token_id"])} + ] + end + end + describe "/addresses/{address_hash}/withdrawals" do test "get empty list on non existing address", %{conn: conn} do address = build(:address) @@ -1660,7 +2229,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end test "check nil", %{conn: conn} do - address = insert(:address, nonce: 1, fetched_coin_balance: 1) + address = insert(:address, transactions_count: 2, fetched_coin_balance: 1) request = get(conn, "/api/v2/addresses") @@ -1914,9 +2483,557 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end end + describe "/addresses/{address_hash}/nft" do + setup do + {:ok, endpoint: &"/api/v2/addresses/#{&1}/nft"} + end + + test "get 404 on non existing address", %{conn: conn, endpoint: endpoint} do + address = build(:address) + + request = get(conn, endpoint.(address.hash)) + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do + request = get(conn, endpoint.("0x")) + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get paginated ERC-721 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + # works because one token_id per token, despite ordering in DB: [asc: ti.token_contract_address_hash, desc: ti.token_id] + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "get paginated ERC-404 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances = + for _ <- 0..50 do + token = insert(:token, type: "ERC-404") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "test filters", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances_721 = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances_1155 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + filter = %{"type" => "ERC-721"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances_721) + + filter = %{"type" => "ERC-1155"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances_1155) + end + + test "return all token instances", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances_721 = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances_1155 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + request_3rd_page = get(conn, endpoint.(address.hash), response_2nd_page["next_page_params"]) + assert response_3rd_page = json_response(request_3rd_page, 200) + + assert response["next_page_params"] != nil + assert response_2nd_page["next_page_params"] != nil + assert response_3rd_page["next_page_params"] == nil + + assert Enum.count(response["items"]) == 50 + assert Enum.count(response_2nd_page["items"]) == 50 + assert Enum.count(response_3rd_page["items"]) == 2 + + compare_item(Enum.at(token_instances_721, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(token_instances_721, 1), Enum.at(response["items"], 49)) + + compare_item(Enum.at(token_instances_721, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(token_instances_1155, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(token_instances_1155, 2), Enum.at(response_2nd_page["items"], 49)) + + compare_item(Enum.at(token_instances_1155, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_3rd_page["items"], 1)) + end + end + + describe "/addresses/{address_hash}/nft/collections" do + setup do + {:ok, endpoint: &"/api/v2/addresses/#{&1}/nft/collections"} + end + + test "get 404 on non existing address", %{conn: conn, endpoint: endpoint} do + address = build(:address) + + request = get(conn, endpoint.(address.hash)) + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do + request = get(conn, endpoint.("0x")) + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get paginated erc-721 collection", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + ctbs = + for _ <- 0..50 do + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + {current_token_balance, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).token_contract_address_hash, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs) + end + + test "get paginated erc-1155 collection", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + collections = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {token, amount, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).contract_address_hash, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, collections) + end + + test "test filters", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + ctbs = + for _ <- 0..50 do + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {current_token_balance, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).token_contract_address_hash, :desc) + + collections = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {token, amount, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).contract_address_hash, :desc) + + filter = %{"type" => "ERC-721"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, ctbs) + + filter = %{"type" => "ERC-1155"} + request = get(conn, endpoint.(address.hash), filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, collections) + end + + test "return all collections", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + insert_list(51, :token_instance) + + collections_721 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {current_token_balance, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).token_contract_address_hash, :desc) + + collections_1155 = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + token_instances = + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(& &1.token_id, :desc) + + {token, amount, token_instances} + end + |> Enum.sort_by(&elem(&1, 0).contract_address_hash, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + request_3rd_page = get(conn, endpoint.(address.hash), response_2nd_page["next_page_params"]) + assert response_3rd_page = json_response(request_3rd_page, 200) + + assert response["next_page_params"] != nil + assert response_2nd_page["next_page_params"] != nil + assert response_3rd_page["next_page_params"] == nil + + assert Enum.count(response["items"]) == 50 + assert Enum.count(response_2nd_page["items"]) == 50 + assert Enum.count(response_3rd_page["items"]) == 2 + + compare_item(Enum.at(collections_721, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(collections_721, 1), Enum.at(response["items"], 49)) + + compare_item(Enum.at(collections_721, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(collections_1155, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(collections_1155, 2), Enum.at(response_2nd_page["items"], 49)) + + compare_item(Enum.at(collections_1155, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(collections_1155, 0), Enum.at(response_3rd_page["items"], 1)) + end + end + defp compare_item(%Address{} = address, json) do assert Address.checksum(address.hash) == json["hash"] - assert to_string(address.nonce + 1) == json["tx_count"] + assert to_string(address.transactions_count) == json["tx_count"] end defp compare_item(%Transaction{} = transaction, json) do @@ -1989,6 +3106,99 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert withdrawal.index == json["index"] end + defp compare_item(%Instance{token: %Token{} = token} = instance, json) do + token_type = token.type + value = to_string(value(token.type, instance)) + id = to_string(instance.token_id) + metadata = instance.metadata + token_address_hash = Address.checksum(token.contract_address_hash) + app_url = instance.metadata["external_url"] + animation_url = instance.metadata["animation_url"] + image_url = instance.metadata["image_url"] + token_name = token.name + + assert %{ + "token_type" => ^token_type, + "value" => ^value, + "id" => ^id, + "metadata" => ^metadata, + "owner" => nil, + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "external_app_url" => ^app_url, + "animation_url" => ^animation_url, + "image_url" => ^image_url, + "is_unique" => nil + } = json + end + + defp compare_item({%CurrentTokenBalance{token: token} = ctb, token_instances}, json) do + token_type = token.type + token_address_hash = Address.checksum(token.contract_address_hash) + token_name = token.name + amount = to_string(ctb.distinct_token_instances_count || ctb.value) + + assert Enum.count(json["token_instances"]) == @instances_amount_in_collection + + token_instances + |> Enum.take(@instances_amount_in_collection) + |> Enum.with_index() + |> Enum.each(fn {instance, index} -> + compare_token_instance_in_collection(instance, Enum.at(json["token_instances"], index)) + end) + + assert %{ + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "amount" => ^amount + } = json + end + + defp compare_item({token, amount, token_instances}, json) do + token_type = token.type + token_address_hash = Address.checksum(token.contract_address_hash) + token_name = token.name + amount = to_string(amount) + + assert Enum.count(json["token_instances"]) == @instances_amount_in_collection + + token_instances + |> Enum.take(@instances_amount_in_collection) + |> Enum.with_index() + |> Enum.each(fn {instance, index} -> + compare_token_instance_in_collection(instance, Enum.at(json["token_instances"], index)) + end) + + assert %{ + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "amount" => ^amount + } = json + end + + defp compare_token_instance_in_collection(%Instance{token: %Token{} = token} = instance, json) do + token_type = token.type + value = to_string(value(token.type, instance)) + id = to_string(instance.token_id) + metadata = instance.metadata + app_url = instance.metadata["external_url"] + animation_url = instance.metadata["animation_url"] + image_url = instance.metadata["image_url"] + + assert %{ + "token_type" => ^token_type, + "value" => ^value, + "id" => ^id, + "metadata" => ^metadata, + "owner" => nil, + "token" => nil, + "external_app_url" => ^app_url, + "animation_url" => ^animation_url, + "image_url" => ^image_url, + "is_unique" => nil + } = json + end + + defp value("ERC-721", _), do: 1 + defp value(_, nft), do: nft.current_token_balance.value + defp check_paginated_response(first_page_resp, second_page_resp, list) do assert Enum.count(first_page_resp["items"]) == 50 assert first_page_resp["next_page_params"] != nil @@ -2015,4 +3225,43 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end def check_total(_, _, _), do: true + + def get_eip1967_implementation_non_zero_address do + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000001"} + end) + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 6804f778f0f7..95584c013bff 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do alias Explorer.Chain.{Address, Block} alias Explorer.Repo + alias Explorer.Tags.AddressTag setup do insert(:block) @@ -171,8 +172,37 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert item["is_verified_via_admin_panel"] == token.is_verified_via_admin_panel end + test "search token by hash", %{conn: conn} do + token = insert(:unique_token) + + request = get(conn, "/api/v2/search?q=#{token.contract_address_hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "token" + assert item["name"] == token.name + assert item["symbol"] == token.symbol + assert item["address"] == Address.checksum(token.contract_address_hash) + assert item["token_url"] =~ Address.checksum(token.contract_address_hash) + assert item["address_url"] =~ Address.checksum(token.contract_address_hash) + assert item["token_type"] == token.type + assert item["is_smart_contract_verified"] == token.contract_address.verified + assert item["exchange_rate"] == (token.fiat_value && to_string(token.fiat_value)) + assert item["total_supply"] == to_string(token.total_supply) + assert item["icon_url"] == token.icon_url + assert item["is_verified_via_admin_panel"] == token.is_verified_via_admin_panel + + item_1 = Enum.at(response["items"], 1) + + assert item_1["type"] == "address" + end + test "search transaction", %{conn: conn} do - tx = insert(:transaction) + tx = insert(:transaction, block_timestamp: nil) request = get(conn, "/api/v2/search?q=#{tx.hash}") assert response = json_response(request, 200) @@ -222,6 +252,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert item["url"] =~ Address.checksum(tag.address.hash) assert item["is_smart_contract_verified"] == tag.address.verified end + + test "check that simultaneous search of ", %{conn: conn} do + block = insert(:block) + + insert(:smart_contract, name: to_string(block.number)) + insert(:token, name: to_string(block.number)) + + insert(:address_to_tag, + tag: %AddressTag{ + label: "qwerty", + display_name: to_string(block.number) + } + ) + + request = get(conn, "/api/v2/search?q=#{block.number}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + end end describe "/search/check-redirect" do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 9c3a1f63254e..c1ed3c3eae65 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -1,5 +1,5 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do - use BlockScoutWeb.ConnCase + use BlockScoutWeb.ConnCase, async: false use BlockScoutWeb.ChannelCase, async: false import Mox @@ -11,6 +11,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do setup :set_mox_from_context + setup :verify_on_exit! + describe "/smart-contracts/{address_hash}" do test "get 404 on non existing SC", %{conn: conn} do address = build(:address) @@ -110,7 +112,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "abi" => target_contract.abi, "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, - "language" => smart_contract_language(target_contract) + "language" => smart_contract_language(target_contract), + "license_type" => "none" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") @@ -154,7 +157,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "stateMutability" => "view", "type" => "function" } - ] + ], + license_type: 13 ) insert(:transaction, @@ -201,7 +205,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "abi" => target_contract.abi, "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, - "language" => smart_contract_language(target_contract) + "language" => smart_contract_language(target_contract), + "license_type" => "gnu_agpl_v3" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") @@ -295,7 +300,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "abi" => target_contract.abi, "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, - "language" => smart_contract_language(target_contract) + "language" => smart_contract_language(target_contract), + "license_type" => "none" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -303,9 +309,130 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert correct_response == response end + + test "get smart-contract multiple additional sources from EIP-1167 implementation", %{conn: conn} do + implementation_contract = + insert(:smart_contract, + external_libraries: [], + constructor_arguments: "", + abi: [ + %{ + "type" => "constructor", + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + license_type: 9 + ) + + insert(:smart_contract_additional_source, + file_name: "test1", + contract_source_code: "test2", + address_hash: implementation_contract.address_hash + ) + + insert(:smart_contract_additional_source, + file_name: "test3", + contract_source_code: "test4", + address_hash: implementation_contract.address_hash + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract.address_hash.bytes, case: :lower) + + proxy_tx_input = + "0x11b804ab000000000000000000000000" <> + implementation_contract_address_hash_string <> + "000000000000000000000000000000000000000000000000000000000000006035323031313537360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284e159163400000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d61100000000000000000000000000ff5ae9b0a7522736299d797d80b8fc6f31d6110000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034420c13696f4ac650b9fafe915553a1abcd7dd300000000000000000000000000000000000000000000000000000000000000184f7074696d69736d2053756273637269626572204e465473000000000000000000000000000000000000000000000000000000000000000000000000000000054f504e46540000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037697066733a2f2f516d66544e504839765651334b5952346d6b52325a6b757756424266456f5a5554545064395538666931503332752f300000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c82bbe41f2cf04e3a8efa18f7032bdd7f6d98a81000000000000000000000000efba8a2a82ec1fb1273806174f5e28fbb917cf9500000000000000000000000000000000000000000000000000000000" + + proxy_deployed_bytecode = + "0x363d3d373d3d3d363d73" <> implementation_contract_address_hash_string <> "5af43d82803e903d91602b57fd5bf3" + + proxy_address = + insert(:contract_address, + contract_code: proxy_deployed_bytecode + ) + + insert(:transaction, + created_contract_address_hash: proxy_address.hash, + input: proxy_tx_input + ) + |> with_block(status: :ok) + + correct_response = %{ + "verified_twin_address_hash" => Address.checksum(implementation_contract.address_hash), + "is_verified" => false, + "is_changed_bytecode" => false, + "is_partially_verified" => implementation_contract.partially_verified, + "is_fully_verified" => false, + "is_verified_via_sourcify" => false, + "is_vyper_contract" => implementation_contract.is_vyper_contract, + "minimal_proxy_address_hash" => Address.checksum("0x" <> implementation_contract_address_hash_string), + "sourcify_repo_url" => nil, + "can_be_visualized_via_sol2uml" => false, + "name" => implementation_contract && implementation_contract.name, + "compiler_version" => implementation_contract.compiler_version, + "optimization_enabled" => implementation_contract.optimization, + "optimization_runs" => implementation_contract.optimization_runs, + "evm_version" => implementation_contract.evm_version, + "verified_at" => implementation_contract.inserted_at |> to_string() |> String.replace(" ", "T"), + "source_code" => implementation_contract.contract_source_code, + "file_path" => implementation_contract.file_path, + "additional_sources" => [ + %{"file_path" => "test1", "source_code" => "test2"}, + %{"file_path" => "test3", "source_code" => "test4"} + ], + "compiler_settings" => implementation_contract.compiler_settings, + "external_libraries" => [], + "constructor_args" => nil, + "decoded_constructor_args" => nil, + "is_self_destructed" => false, + "deployed_bytecode" => proxy_deployed_bytecode, + "creation_bytecode" => proxy_tx_input, + "abi" => implementation_contract.abi, + "is_verified_via_eth_bytecode_db" => implementation_contract.verified_via_eth_bytecode_db, + "language" => smart_contract_language(implementation_contract), + "license_type" => "bsd_3_clause" + } + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") + response = json_response(request, 200) + + assert correct_response == response + end end describe "/smart-contracts/{address_hash} <> eth_bytecode_db" do + setup do + old_interval_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) + + :ok + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_interval_env) + end) + end + test "automatically verify contract", %{conn: conn} do {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) old_chain_id = Application.get_env(:block_scout_web, :chain_id) @@ -319,7 +446,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, service_url: "http://localhost:#{bypass.port}", - enabled: true + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true ) address = insert(:contract_address) @@ -344,6 +473,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + assert_receive %Phoenix.Socket.Message{ payload: %{}, event: "smart_contract_was_verified", @@ -389,7 +525,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, service_url: "http://localhost:#{bypass.port}", - enabled: true + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true ) address = insert(:contract_address) @@ -414,6 +552,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + assert_receive %Phoenix.Socket.Message{ payload: %{}, event: "smart_contract_was_verified", @@ -506,7 +651,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, service_url: "http://localhost:#{bypass.port}", - enabled: true + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true ) address = insert(:contract_address) @@ -531,6 +678,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + assert_receive %Phoenix.Socket.Message{ payload: %{}, event: "smart_contract_was_verified", @@ -580,7 +734,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, service_url: "http://localhost:#{bypass.port}", - enabled: true + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true ) address = insert(:contract_address) @@ -605,6 +761,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + assert_receive %Phoenix.Socket.Message{ payload: %{}, event: "smart_contract_was_verified", @@ -671,6 +834,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do bypass = Bypass.open() address = insert(:contract_address) + topic = "addresses:#{address.hash}" + + {:ok, _reply, _socket} = + BlockScoutWeb.UserSocketV2 + |> socket("no_id", %{}) + |> subscribe_and_join(topic) insert(:transaction, created_contract_address_hash: address.hash, @@ -683,7 +852,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, service_url: "http://localhost:#{bypass.port}", - enabled: true + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true ) old_interval_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) @@ -696,6 +867,20 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + :timer.sleep(10) Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search", fn conn -> @@ -704,6 +889,20 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + :timer.sleep(10) Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search", fn conn -> @@ -712,12 +911,47 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + assert_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + :timer.sleep(10) Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, fetch_interval: 10000) _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + refute_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "eth_bytecode_db_lookup_started", + topic: ^topic + }, + :timer.seconds(1) + + refute_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_not_verified", + topic: ^topic + }, + :timer.seconds(1) + + refute_receive %Phoenix.Socket.Message{ + payload: %{}, + event: "smart_contract_was_verified", + topic: ^topic + }, + :timer.seconds(1) + Application.put_env(:block_scout_web, :chain_id, old_chain_id) Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_interval_env) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) @@ -892,13 +1126,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "tuple[bytes32,uint256,bytes32,uint256,address,address,uint256,bool,tuple[address,bytes32[],bytes][]]", "value" => [ "0xfe6a43fa23a0269092cbf97cb908e1d5a49a18fd6942baf2467fb5b221e39ab2", - 1000, + "1000", "0xfe6a43fa23a0269092cbf97cb908e1d5a49a18fd6942baf2467fb5b221e39ab2", - 10, + "10", "0xbb36c792b9b45aaf8b848a1392b0d6559202729e", "0xbb36c792b9b45aaf8b848a1392b0d6559202729e", - 123_123, - true, + "123123", + "true", [ [ "0xbb36c792b9b45aaf8b848a1392b0d6559202729e", @@ -939,6 +1173,64 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do refute %{"type" => "receive"} in response end + test "ensure read-methods are not duplicated", %{conn: conn} do + abi = [ + %{ + "inputs" => [], + "name" => "test", + "outputs" => [ + %{"internalType" => "uint256", "name" => "", "type" => "uint256"} + ], + "stateMutability" => "pure", + "type" => "function" + } + ] + + id = + abi + |> ABI.parse_specification() + |> Enum.at(0) + |> Map.fetch!(:method_id) + + target_contract = insert(:smart_contract, abi: abi) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: _params}], _opts -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000009d37020ac9049a8040" + } + ]} + end + ) + + request = get(conn, "/api/v2/smart-contracts/#{target_contract.address_hash}/methods-read") + + assert response = json_response(request, 200) + + assert response == [ + %{ + "type" => "function", + "stateMutability" => "pure", + "outputs" => [ + %{ + "type" => "uint256", + "value" => "2900102562052921000000" + } + ], + "name" => "test", + "names" => ["uint256"], + "inputs" => [], + "method_id" => Base.encode16(id, case: :lower) + } + ] + end + test "get array of addresses within read-methods", %{conn: conn} do abi = [ %{ @@ -1243,7 +1535,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert %{ "is_error" => false, - "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => true}]} + "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => "true"}]} } == response end @@ -1344,13 +1636,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "tuple[bytes32,uint256,bytes32,uint256,address,address,uint256,bool,tuple[address,bytes32[],bytes][]]", "value" => [ "0xfe6a43fa23a0269092cbf97cb908e1d5a49a18fd6942baf2467fb5b221e39ab2", - 1000, + "1000", "0xfe6a43fa23a0269092cbf97cb908e1d5a49a18fd6942baf2467fb5b221e39ab2", - 10, + "10", "0xbb36c792b9b45aaf8b848a1392b0d6559202729e", "0xbb36c792b9b45aaf8b848a1392b0d6559202729e", - 123_123, - true, + "123123", + "true", [ [ "0xbb36c792b9b45aaf8b848a1392b0d6559202729e", @@ -1853,7 +2145,89 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert %{ "is_error" => false, - "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => true}]} + "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => "true"}]} + } == response + end + + test "query read method 1", %{conn: conn} do + abi = [ + %{ + "inputs" => [ + %{ + "internalType" => "uint256", + "name" => "amountIn", + "type" => "uint256" + }, + %{ + "internalType" => "address[]", + "name" => "path", + "type" => "address[]" + } + ], + "name" => "getAmountsOut", + "outputs" => [ + %{ + "internalType" => "uint256[]", + "name" => "amounts", + "type" => "uint256[]" + } + ], + "stateMutability" => "view", + "type" => "function" + } + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0xd06ca61f00000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000909fd75ce23a7e61787fe2763652935f921164610000000000000000000000009801eeb848987c0a8d6443912827bd36c288f8fb" + }, + _ + ] + } + ], + _opts -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000000037240fc3496a65" + } + ]} + end + ) + + target_contract = insert(:smart_contract, abi: abi) + + request = + post(conn, "/api/v2/smart-contracts/#{target_contract.address_hash}/query-read-method", %{ + "contract_type" => "regular", + "args" => [ + "1000000000000000000000", + ["0x909Fd75Ce23a7e61787FE2763652935F92116461", "0x9801eeb848987c0a8d6443912827bd36c288f8fb"] + ], + "method_id" => "d06ca61f" + }) + + assert response = json_response(request, 200) + + assert %{ + "is_error" => false, + "result" => %{ + "names" => ["amounts"], + "output" => [ + %{"type" => "uint256[]", "value" => ["1000000000000000000000", "15520773838563941"]} + ] + } } == response end end @@ -1907,18 +2281,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do target_contract = insert(:smart_contract, abi: abi) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} - end) + mock_logic_storage_pointer_request(target_contract.address_hash) expect( EthereumJSONRPC.Mox, @@ -2017,18 +2380,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do target_contract = insert(:smart_contract, abi: abi) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} - end) + mock_logic_storage_pointer_request(target_contract.address_hash) expect( EthereumJSONRPC.Mox, @@ -2071,7 +2423,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert %{ "is_error" => false, - "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => true}]} + "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => "true"}]} } == response end @@ -2102,18 +2454,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do target_contract = insert(:smart_contract, abi: abi) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} - end) + mock_logic_storage_pointer_request(target_contract.address_hash) expect( EthereumJSONRPC.Mox, @@ -2171,18 +2512,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do target_contract = insert(:smart_contract, abi: abi) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} - end) + mock_logic_storage_pointer_request(target_contract.address_hash) expect( EthereumJSONRPC.Mox, @@ -2239,18 +2569,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do target_contract = insert(:smart_contract, abi: abi) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} - end) + mock_logic_storage_pointer_request(target_contract.address_hash) expect( EthereumJSONRPC.Mox, @@ -2331,18 +2650,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do target_contract = insert(:smart_contract, abi: abi) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} - end) + mock_logic_storage_pointer_request(target_contract.address_hash) contract = insert(:smart_contract) @@ -2394,6 +2702,107 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do check_paginated_response(response, response_2nd_page, smart_contracts) end + + test "ignores wrong ordering params", %{conn: conn} do + smart_contracts = + for _ <- 0..50 do + insert(:smart_contract) + end + + ordering_params = %{"sort" => "foo", "order" => "bar"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by balance ascending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, fetched_coin_balance: i) + insert(:smart_contract, address_hash: address.hash, address: address) + end + |> Enum.reverse() + + ordering_params = %{"sort" => "balance", "order" => "asc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by balance descending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, fetched_coin_balance: i) + insert(:smart_contract, address_hash: address.hash, address: address) + end + + ordering_params = %{"sort" => "balance", "order" => "desc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by transaction count ascending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, transactions_count: i) + insert(:smart_contract, address_hash: address.hash, address: address) + end + |> Enum.reverse() + + ordering_params = %{"sort" => "txs_count", "order" => "asc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end + + test "can order by transaction count descending", %{conn: conn} do + smart_contracts = + for i <- 0..50 do + address = insert(:address, transactions_count: i) + insert(:smart_contract, address_hash: address.hash, address: address) + end + + ordering_params = %{"sort" => "txs_count", "order" => "desc"} + + request = get(conn, "/api/v2/smart-contracts", ordering_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, smart_contracts) + end end describe "/smart-contracts/counters" do @@ -2460,4 +2869,19 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "solidity" end end + + defp mock_logic_storage_pointer_request(address_hash) do + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{address_hash |> to_string() |> String.replace("0x", "")}"} + end) + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index 05b7009487c4..9136d8053b78 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -160,6 +160,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) end @@ -192,7 +193,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block: tx.block, block_number: tx.block_number, token_contract_address: token.contract_address, - token_ids: [i] + token_ids: [i], + token_type: "ERC-721" ) end @@ -218,6 +220,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) @@ -250,6 +253,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx_1.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..24, fn x -> x end) ) @@ -267,6 +271,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx_2.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(25..49, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(25..49, fn x -> x end) ) @@ -282,6 +287,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx_2.block_number, token_contract_address: token.contract_address, token_ids: [50], + token_type: "ERC-1155", amounts: [50] ) @@ -308,6 +314,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx_1.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..24, fn x -> x end) ) @@ -325,6 +332,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx_2.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(25..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(25..50, fn x -> x end) ) @@ -524,6 +532,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do tokens_ordered_by_holders_asc ) + :timer.sleep(200) + # by circulating_market_cap tokens_ordered_by_circulating_market_cap = Enum.sort(tokens, &(&1.circulating_market_cap <= &2.circulating_market_cap)) @@ -593,6 +603,23 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) end + test "ignores wrong ordering params", %{conn: conn} do + tokens = + for i <- 0..50 do + insert(:token, fiat_value: i) + end + + request = get(conn, "/api/v2/tokens", %{"sort" => "foo", "order" => "bar"}) + + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens", %{"sort" => "foo", "order" => "bar"} |> Map.merge(response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, tokens) + end + test "tokens are filtered by single type", %{conn: conn} do erc_20_tokens = for i <- 0..50 do @@ -609,14 +636,20 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do insert(:token, type: "ERC-1155") end - check_tokens_pagination(erc_20_tokens |> Enum.reverse(), conn, %{"type" => "ERC-20"}) + erc_404_tokens = + for _i <- 0..50 do + insert(:token, type: "ERC-404") + end + + check_tokens_pagination(erc_20_tokens, conn, %{"type" => "ERC-20"}) check_tokens_pagination(erc_721_tokens |> Enum.reverse(), conn, %{"type" => "ERC-721"}) check_tokens_pagination(erc_1155_tokens |> Enum.reverse(), conn, %{"type" => "ERC-1155"}) + check_tokens_pagination(erc_404_tokens |> Enum.reverse(), conn, %{"type" => "ERC-404"}) end test "tokens are filtered by multiple type", %{conn: conn} do erc_20_tokens = - for i <- 0..25 do + for i <- 11..36 do insert(:token, fiat_value: i) end @@ -630,6 +663,11 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do insert(:token, type: "ERC-1155") end + erc_404_tokens = + for _i <- 0..24 do + insert(:token, type: "ERC-404") + end + check_tokens_pagination( erc_721_tokens |> Kernel.++(erc_1155_tokens) |> Enum.reverse(), conn, @@ -639,12 +677,20 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do ) check_tokens_pagination( - erc_20_tokens |> Kernel.++(erc_1155_tokens) |> Enum.reverse(), + erc_1155_tokens |> Enum.reverse() |> Kernel.++(erc_20_tokens), conn, %{ "type" => "[erc-20,ERC-1155]" } ) + + check_tokens_pagination( + erc_404_tokens |> Enum.reverse() |> Kernel.++(erc_20_tokens), + conn, + %{ + "type" => "[erc-20,ERC-404]" + } + ) end test "sorting by fiat_value", %{conn: conn} do @@ -652,7 +698,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do for i <- 0..50 do insert(:token, fiat_value: i) end - |> Enum.reverse() check_tokens_pagination(tokens, conn) end @@ -865,6 +910,87 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do check_paginated_response(response, response_2nd_page, instances) end + + test "get instances list by holder erc-721", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + insert_list(51, :token_instance, token_contract_address_hash: token.contract_address_hash) + + address = insert(:address, contract_code: Enum.random([nil, "0x010101"])) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token, :owner]) + end + + filter = %{"holder_address_hash" => to_string(address.hash)} + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end + + test "get instances list by holder erc-1155", %{conn: conn} do + token = insert(:token, type: "ERC-1155") + + insert_list(51, :token_instance, token_contract_address_hash: token.contract_address_hash) + + address = insert(:address, contract_code: Enum.random([nil, "0x010101"])) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..2) + ) + + %Instance{ti | current_token_balance: current_token_balance, owner: address} + end + + filter = %{"holder_address_hash" => to_string(address.hash)} + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + end end describe "/tokens/{address_hash}/instances/{token_id}" do @@ -896,11 +1022,12 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do instance = insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash) - transfer = + _transfer = insert(:token_transfer, token_contract_address: token.contract_address, transaction: transaction, - token_ids: [0] + token_ids: [0], + token_type: "ERC-721" ) for _ <- 1..50 do @@ -911,7 +1038,27 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert data = json_response(request, 200) assert compare_item(instance, data) - assert compare_item(transfer.to_address, data["owner"]) + assert Address.checksum(instance.owner_address_hash) == data["owner"]["hash"] + end + + test "get token instance by token id which is not presented in DB", %{conn: conn} do + token = insert(:token, type: "ERC-721") + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0") + token_address = Address.checksum(token.contract_address.hash) + token_name = token.name + token_type = token.type + + assert %{ + "animation_url" => nil, + "external_app_url" => nil, + "id" => "0", + "image_url" => nil, + "is_unique" => true, + "metadata" => nil, + "owner" => nil, + "token" => %{"address" => ^token_address, "name" => ^token_name, "type" => ^token_type} + } = json_response(request, 200) end end @@ -950,6 +1097,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do token_contract_address: token.contract_address, transaction: transaction, token_ids: [id + 1], + token_type: "ERC-1155", amounts: [1] ) @@ -958,6 +1106,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do token_contract_address: token.contract_address, transaction: transaction, token_ids: [id, id + 1], + token_type: "ERC-1155", amounts: [1, 2] ) @@ -971,7 +1120,70 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do insert(:token_transfer, token_contract_address: token.contract_address, transaction: transaction, - token_ids: [id] + token_ids: [id], + token_type: "ERC-1155" + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/tokens/#{token.contract_address_hash}/instances/#{id}/transfers", + response["next_page_params"] + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, transfers_0 ++ transfers_1) + end + + test "check that pagination works for 404 tokens", %{conn: conn} do + token = insert(:token, type: "ERC-404") + + for _ <- 0..50 do + insert(:token_instance, token_id: 0) + end + + id = :rand.uniform(1_000_000) + + transaction = + :transaction + |> insert(input: "0xabcd010203040506") + |> with_block() + + insert(:token_instance, token_id: id, token_contract_address_hash: token.contract_address_hash) + + insert_list(100, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id + 1], + token_type: "ERC-404", + amounts: [1] + ) + + transfers_0 = + insert_list(26, :token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id, id + 1], + token_type: "ERC-404", + amounts: [1, 2] + ) + + transfers_1 = + for _ <- 26..50 do + transaction = + :transaction + |> insert(input: "0xabcd010203040506") + |> with_block() + + insert(:token_transfer, + token_contract_address: token.contract_address, + transaction: transaction, + token_ids: [id], + token_type: "ERC-404" ) end @@ -1003,7 +1215,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block: tx.block, block_number: tx.block_number, token_contract_address: token.contract_address, - token_ids: [id] + token_ids: [id], + token_type: "ERC-721" ) end @@ -1038,6 +1251,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) @@ -1066,6 +1280,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn x -> x end) ++ [id], + token_type: "ERC-1155", amounts: Enum.map(1..51, fn x -> x end) ++ [amount] ) end @@ -1195,7 +1410,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do insert_list(count, :token_transfer, token_contract_address: token.contract_address, transaction: transaction, - token_ids: [0] + token_ids: [0], + token_type: "ERC-721" ) request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/0/transfers-count") @@ -1240,6 +1456,40 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do compare_item(Repo.preload(ctb, [{:token, :contract_address}]).token, json["token"]) end + def compare_item(%Instance{token: %Token{} = token} = instance, json) do + token_type = token.type + value = to_string(value(token.type, instance)) + id = to_string(instance.token_id) + metadata = instance.metadata + token_address_hash = Address.checksum(token.contract_address_hash) + app_url = instance.metadata["external_url"] + animation_url = instance.metadata["animation_url"] + image_url = instance.metadata["image_url"] + token_name = token.name + owner_address_hash = Address.checksum(instance.owner.hash) + is_contract = !is_nil(instance.owner.contract_code) + is_unique = value == "1" + + assert %{ + "token_type" => ^token_type, + "value" => ^value, + "id" => ^id, + "metadata" => ^metadata, + "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "external_app_url" => ^app_url, + "animation_url" => ^animation_url, + "image_url" => ^image_url, + "is_unique" => ^is_unique + } = json + + if is_unique do + assert owner_address_hash == json["owner"]["hash"] + assert is_contract == json["owner"]["is_contract"] + else + assert json["owner"] == nil + end + end + def compare_item(%Instance{} = instance, json) do assert to_string(instance.token_id) == json["id"] assert Jason.decode!(Jason.encode!(instance.metadata)) == json["metadata"] @@ -1247,6 +1497,9 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do compare_item(Repo.preload(instance, [{:token, :contract_address}]).token, json["token"]) end + defp value("ERC-721", _), do: 1 + defp value(_, nft), do: nft.current_token_balance.value + # with the current implementation no transfers should come with list in totals def check_total(%Token{type: nft}, json, _token_transfer) when nft in ["ERC-721", "ERC-1155"] and is_list(json) do false diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index f718a35b4770..9e93bc58654b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -8,6 +8,13 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction} alias Explorer.Repo + @first_topic_hex_string_1 "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) @@ -238,6 +245,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) @@ -264,6 +272,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: [1], + token_type: "ERC-1155", amounts: [2], amount: nil ) @@ -574,7 +583,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block: tx.block, block_number: tx.block_number, token_contract_address: erc_1155_token.contract_address, - token_ids: [x] + token_ids: [x], + token_type: "ERC-1155" ) end |> Enum.reverse() @@ -588,7 +598,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block: tx.block, block_number: tx.block_number, token_contract_address: erc_721_token.contract_address, - token_ids: [x] + token_ids: [x], + token_type: "ERC-721" ) end |> Enum.reverse() @@ -601,7 +612,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do transaction: tx, block: tx.block, block_number: tx.block_number, - token_contract_address: erc_20_token.contract_address + token_contract_address: erc_20_token.contract_address, + token_type: "ERC-20" ) end |> Enum.reverse() @@ -716,6 +728,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn _x -> id end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) end @@ -751,7 +764,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block: tx.block, block_number: tx.block_number, token_contract_address: token.contract_address, - token_ids: [i] + token_ids: [i], + token_type: "ERC-721" ) end @@ -781,6 +795,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..50, fn x -> x end) ) @@ -816,6 +831,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..24, fn x -> x end) ) @@ -831,6 +847,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(25..49, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(25..49, fn x -> x end) ) @@ -846,6 +863,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: [50], + token_type: "ERC-1155", amounts: [50] ) @@ -872,6 +890,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(0..24, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(0..24, fn x -> x end) ) @@ -887,6 +906,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: tx.block_number, token_contract_address: token.contract_address, token_ids: Enum.map(25..50, fn x -> x end), + token_type: "ERC-1155", amounts: Enum.map(25..50, fn x -> x end) ) @@ -954,142 +974,132 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end end - describe "stability fees" do - setup %{conn: conn} do - old_env = Application.get_env(:explorer, :chain_type) - - Application.put_env(:explorer, :chain_type, "stability") + if Application.compile_env(:explorer, :chain_type) == "stability" do + describe "stability fees" do + test "check stability fees", %{conn: conn} do + tx = insert(:transaction) |> with_block() - on_exit(fn -> - Application.put_env(:explorer, :chain_type, old_env) - end) - - %{conn: conn} - end - - test "check stability fees", %{conn: conn} do - tx = insert(:transaction) |> with_block() - - log = - insert(:log, - transaction: tx, - index: 1, - block: tx.block, - block_number: tx.block_number, - first_topic: "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155", - data: - "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" - ) - - insert(:token, contract_address: build(:address, hash: "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43")) - request = get(conn, "/api/v2/transactions") + _log = + insert(:log, + transaction: tx, + index: 1, + block: tx.block, + block_number: tx.block_number, + first_topic: topic(@first_topic_hex_string_1), + data: + "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" + ) - assert %{ - "items" => [ - %{ - "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, - "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, - "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, - "total_fee" => "44136000000000", - "dapp_fee" => "22068000000000", - "validator_fee" => "22068000000000" + insert(:token, contract_address: build(:address, hash: "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43")) + request = get(conn, "/api/v2/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}") + + assert %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" } - ] - } = json_response(request, 200) - - request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}") - - assert %{ - "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, - "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, - "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, - "total_fee" => "44136000000000", - "dapp_fee" => "22068000000000", - "validator_fee" => "22068000000000" - } - } = json_response(request, 200) - - request = get(conn, "/api/v2/addresses/#{to_string(tx.from_address_hash)}/transactions") - - assert %{ - "items" => [ - %{ - "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, - "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, - "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, - "total_fee" => "44136000000000", - "dapp_fee" => "22068000000000", - "validator_fee" => "22068000000000" + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(tx.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } } - } - ] - } = json_response(request, 200) - end - - test "check stability if token absent in DB", %{conn: conn} do - tx = insert(:transaction) |> with_block() + ] + } = json_response(request, 200) + end - log = - insert(:log, - transaction: tx, - index: 1, - block: tx.block, - block_number: tx.block_number, - first_topic: "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155", - data: - "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" - ) + test "check stability if token absent in DB", %{conn: conn} do + tx = insert(:transaction) |> with_block() - request = get(conn, "/api/v2/transactions") + _log = + insert(:log, + transaction: tx, + index: 1, + block: tx.block, + block_number: tx.block_number, + first_topic: topic(@first_topic_hex_string_1), + data: + "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" + ) - assert %{ - "items" => [ - %{ - "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, - "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, - "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, - "total_fee" => "44136000000000", - "dapp_fee" => "22068000000000", - "validator_fee" => "22068000000000" + request = get(conn, "/api/v2/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } } + ] + } = json_response(request, 200) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}") + + assert %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" } - ] - } = json_response(request, 200) - - request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}") - - assert %{ - "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, - "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, - "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, - "total_fee" => "44136000000000", - "dapp_fee" => "22068000000000", - "validator_fee" => "22068000000000" - } - } = json_response(request, 200) - - request = get(conn, "/api/v2/addresses/#{to_string(tx.from_address_hash)}/transactions") - - assert %{ - "items" => [ - %{ - "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, - "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, - "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, - "total_fee" => "44136000000000", - "dapp_fee" => "22068000000000", - "validator_fee" => "22068000000000" + } = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{to_string(tx.from_address_hash)}/transactions") + + assert %{ + "items" => [ + %{ + "stability_fee" => %{ + "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, + "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, + "total_fee" => "44136000000000", + "dapp_fee" => "22068000000000", + "validator_fee" => "22068000000000" + } } - } - ] - } = json_response(request, 200) + ] + } = json_response(request, 200) + end end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs new file mode 100644 index 000000000000..8b80a9541cb5 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs @@ -0,0 +1,130 @@ +defmodule BlockScoutWeb.API.V2.UtilsControllerTest do + use BlockScoutWeb.ConnCase + + import Mox + + describe "/api/v2/utils/decode-calldata" do + test "success decodes calldata", %{conn: conn} do + transaction = + :transaction_to_verified_contract + |> insert() + + request_zero_implementations() + + assert conn + |> get("/api/v2/utils/decode-calldata", %{ + "calldata" => to_string(transaction.input), + "address_hash" => to_string(transaction.to_address) + }) + |> json_response(200) == + %{ + "result" => %{ + "method_call" => "set(uint256 x)", + "method_id" => "60fe47b1", + "parameters" => [%{"name" => "x", "type" => "uint256", "value" => "50"}] + } + } + + request_zero_implementations() + + assert conn + |> post("/api/v2/utils/decode-calldata", %{ + "calldata" => to_string(transaction.input), + "address_hash" => to_string(transaction.to_address) + }) + |> json_response(200) == + %{ + "result" => %{ + "method_call" => "set(uint256 x)", + "method_id" => "60fe47b1", + "parameters" => [%{"name" => "x", "type" => "uint256", "value" => "50"}] + } + } + end + + test "return nil in case of failed decoding", %{conn: conn} do + assert conn + |> post("/api/v2/utils/decode-calldata", %{ + "calldata" => "0x010101" + }) + |> json_response(200) == + %{ + "result" => nil + } + end + + test "decodes using ABI from smart_contracts_methods table", %{conn: conn} do + insert(:contract_method) + + input_data = + "set(uint)" + |> ABI.encode([50]) + |> Base.encode16(case: :lower) + + assert conn + |> post("/api/v2/utils/decode-calldata", %{ + "calldata" => "0x" <> input_data + }) + |> json_response(200) == + %{ + "result" => %{ + "method_call" => "set(uint256 x)", + "method_id" => "60fe47b1", + "parameters" => [%{"name" => "x", "type" => "uint256", "value" => "50"}] + } + } + end + end + + defp request_zero_implementations do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs new file mode 100644 index 000000000000..9981c830ca2f --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs @@ -0,0 +1,205 @@ +defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Address + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + alias Explorer.Chain.Cache.StabilityValidatorsCounters + + if Application.compile_env(:explorer, :chain_type) == "stability" do + describe "/validators/stability" do + test "get paginated list of the validators", %{conn: conn} do + validators = + insert_list(51, :validator_stability) + |> Enum.sort_by( + fn validator -> + {Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} + end, + :desc + ) + + request = get(conn, "/api/v2/validators/stability") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/validators/stability", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + + test "sort by blocks_validated asc", %{conn: conn} do + validators = + for _ <- 0..50 do + validator = insert(:validator_stability) + blocks_count = Enum.random(0..50) + + _ = + for _ <- 0..blocks_count do + insert(:block, miner_hash: validator.address_hash, miner: nil) + end + + {validator, blocks_count} + end + |> Enum.sort(&compare_default_sorting_for_asc/2) + + init_params = %{"sort" => "blocks_validated", "order" => "asc"} + request = get(conn, "/api/v2/validators/stability", init_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + + test "sort by blocks_validated desc", %{conn: conn} do + validators = + for _ <- 0..50 do + validator = insert(:validator_stability) + blocks_count = Enum.random(0..50) + + _ = + for _ <- 0..blocks_count do + insert(:block, miner_hash: validator.address_hash, miner: nil) + end + + {validator, blocks_count} + end + |> Enum.sort(&compare_default_sorting_for_desc/2) + + init_params = %{"sort" => "blocks_validated", "order" => "desc"} + request = get(conn, "/api/v2/validators/stability", init_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + + test "state_filter=probation", %{conn: conn} do + insert_list(51, :validator_stability, state: Enum.random([:active, :inactive])) + + validators = + insert_list(51, :validator_stability, state: :probation) + |> Enum.sort_by( + fn validator -> + {Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} + end, + :desc + ) + + init_params = %{"state_filter" => "probation"} + + request = get(conn, "/api/v2/validators/stability", init_params) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/validators/stability", Map.merge(init_params, response["next_page_params"])) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, validators) + end + end + + describe "/validators/stability/counters" do + test "get counters", %{conn: conn} do + _validator_active1 = + insert(:validator_stability, state: :active, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) + + _validator_active2 = insert(:validator_stability, state: :active) + _validator_active3 = insert(:validator_stability, state: :active) + + _validator_inactive1 = + insert(:validator_stability, state: :inactive, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) + + _validator_inactive2 = insert(:validator_stability, state: :inactive) + _validator_inactive3 = insert(:validator_stability, state: :inactive) + + _validator_probation1 = + insert(:validator_stability, state: :probation, inserted_at: DateTime.add(DateTime.utc_now(), -2, :day)) + + _validator_probation2 = insert(:validator_stability, state: :probation) + _validator_probation3 = insert(:validator_stability, state: :probation) + + StabilityValidatorsCounters.consolidate() + :timer.sleep(500) + + percentage = (3 / 9 * 100) |> Float.floor(2) + request = get(conn, "/api/v2/validators/stability/counters") + + assert %{ + "active_validators_counter" => "3", + "active_validators_percentage" => ^percentage, + "new_validators_counter_24h" => "6", + "validators_counter" => "9" + } = json_response(request, 200) + end + end + end + + defp compare_item(%ValidatorStability{} = validator, json) do + assert Address.checksum(validator.address_hash) == json["address"]["hash"] + assert to_string(validator.state) == json["state"] + end + + defp compare_item({%ValidatorStability{} = validator, count}, json) do + assert json["blocks_validated_count"] == count + 1 + assert compare_item(validator, json) + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp compare_default_sorting_for_asc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do + case { + compare(blocks_count_1, blocks_count_2), + compare( + Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state), + Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state) + ), + compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes) + } do + {:lt, _, _} -> false + {:eq, :lt, _} -> false + {:eq, :eq, :lt} -> false + _ -> true + end + end + + defp compare_default_sorting_for_desc({validator_1, blocks_count_1}, {validator_2, blocks_count_2}) do + case { + compare(blocks_count_1, blocks_count_2), + compare( + Keyword.fetch!(ValidatorStability.state_enum(), validator_1.state), + Keyword.fetch!(ValidatorStability.state_enum(), validator_2.state) + ), + compare(validator_1.address_hash.bytes, validator_2.address_hash.bytes) + } do + {:gt, _, _} -> false + {:eq, :lt, _} -> false + {:eq, :eq, :lt} -> false + _ -> true + end + end + + defp compare(a, b) do + cond do + a < b -> :lt + a > b -> :gt + true -> :eq + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs index 5a52e764a3cd..8d2889876964 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs @@ -86,6 +86,8 @@ defmodule BlockScoutWeb.SmartContractControllerTest do contract_code_md5: "123" ) + get_eip1967_implementation_zero_addresses() + path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: token_contract_address.hash, @@ -276,62 +278,99 @@ defmodule BlockScoutWeb.SmartContractControllerTest do end defp blockchain_get_implementation_mock do - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn %{id: _, method: _, params: [_, _, _]}, _options -> - {:ok, "0xcebb2CCCFe291F0c442841cBE9C1D06EED61Ca02"} - end - ) + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0xcebb2CCCFe291F0c442841cBE9C1D06EED61Ca02"} + end) end defp blockchain_get_implementation_mock_2 do - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn %{id: _, method: _, params: [_, _, _]}, _options -> - {:ok, "0x000000000000000000000000cebb2CCCFe291F0c442841cBE9C1D06EED61Ca02"} - end - ) + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000cebb2CCCFe291F0c442841cBE9C1D06EED61Ca02"} + end) end - def get_eip1967_implementation do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> + defp mock_empty_logic_storage_pointer_request do + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", - "latest" - ] - }, - _options -> + end + + defp mock_empty_beacon_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", - "latest" - ] - }, - _options -> + end + + defp mock_empty_eip_1822_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) end + + defp mock_empty_oz_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + def get_eip1967_implementation_zero_addresses do + mock_empty_logic_storage_pointer_request() + |> mock_empty_beacon_storage_pointer_request() + |> mock_empty_oz_storage_pointer_request() + |> mock_empty_eip_1822_storage_pointer_request() + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs index 799b54da797c..e15f85569214 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.exs @@ -126,7 +126,7 @@ defmodule BlockScoutWeb.Tokens.InventoryControllerTest do transaction: transaction, token_contract_address: token.contract_address, token: token, - token_id: 1000 + token_ids: [1000] ) conn = get(conn, token_inventory_path(conn, :index, token.contract_address_hash), %{type: "JSON"}) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs index ab5ba16f9c5d..7a03cfba35f7 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs @@ -3,6 +3,8 @@ defmodule BlockScoutWeb.Tokens.ContractControllerTest do import Mox + setup :verify_on_exit! + describe "GET index/3" do test "with invalid address hash", %{conn: conn} do conn = get(conn, token_read_contract_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) @@ -53,7 +55,7 @@ defmodule BlockScoutWeb.Tokens.ContractControllerTest do token: token ) - get_eip1967_implementation() + request_zero_implementations() conn = get(conn, token_read_contract_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)) @@ -62,7 +64,7 @@ defmodule BlockScoutWeb.Tokens.ContractControllerTest do end end - def get_eip1967_implementation do + def request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -100,5 +102,17 @@ defmodule BlockScoutWeb.Tokens.ContractControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs index 4969db368530..71deea5dbf43 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs @@ -1,8 +1,6 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do use BlockScoutWeb.ConnCase - import Mox - import BlockScoutWeb.WebRouter.Helpers, only: [transaction_internal_transaction_path: 3] alias Explorer.Chain.InternalTransaction @@ -75,11 +73,6 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do end test "includes USD exchange rate value for address in assigns", %{conn: conn} do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} - end) - transaction = insert(:transaction) conn = get(conn, transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs index fc9ced3bba5f..57f8933c243c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs @@ -1,8 +1,6 @@ defmodule BlockScoutWeb.TransactionLogControllerTest do use BlockScoutWeb.ConnCase - import Mox - import BlockScoutWeb.WebRouter.Helpers, only: [transaction_log_path: 3] alias Explorer.Chain.Address @@ -157,11 +155,6 @@ defmodule BlockScoutWeb.TransactionLogControllerTest do end test "includes USD exchange rate value for address in assigns", %{conn: conn} do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} - end) - transaction = insert(:transaction) conn = get(conn, transaction_log_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) @@ -170,11 +163,6 @@ defmodule BlockScoutWeb.TransactionLogControllerTest do end test "loads for transactions that created a contract", %{conn: conn} do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} - end) - contract_address = insert(:contract_address) transaction = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs index ac5898f37f37..15217a697e11 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2] import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain.Wei - alias Indexer.Fetcher.CoinBalance + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Explorer.Counters.{AddressesCounter, AverageBlockTime} alias Indexer.Fetcher.CoinBalanceOnDemand @@ -182,7 +182,7 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do test "fetch coin balances if needed", %{conn: conn} do json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) EthereumJSONRPC.Mox |> stub(:json_rpc, fn diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs index 6b789a2d0f4c..398d84dd8acd 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs @@ -7,13 +7,10 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do alias Explorer.ExchangeRates.Token + setup :verify_on_exit! + describe "GET index/3" do test "load token transfers", %{conn: conn} do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} - end) - transaction = insert(:transaction) token_transfer = insert(:token_transfer, transaction: transaction) @@ -71,11 +68,6 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do end test "includes USD exchange rate value for address in assigns", %{conn: conn} do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} - end) - transaction = insert(:transaction) conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash)) @@ -201,8 +193,17 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) transaction = insert(:transaction_to_verified_contract) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs index 0a68febfa183..55936934dcdb 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs @@ -65,7 +65,9 @@ defmodule BlockScoutWeb.VerifiedContractsControllerTest do expected_path = verified_contracts_path(conn, :index, %{ smart_contract_id: id, - items_count: "50" + items_count: "50", + coin_balance: nil, + tx_count: nil }) assert Map.get(json_response(conn, 200), "next_page_path") == expected_path diff --git a/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs b/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs index ea20aa4f4ede..d10a3fdcc639 100644 --- a/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/schema/query/token_transfers_test.exs @@ -16,7 +16,6 @@ defmodule BlockScoutWeb.Schema.Query.TokenTransfersTest do amounts block_number log_index - token_id token_ids from_address_hash to_address_hash @@ -45,7 +44,6 @@ defmodule BlockScoutWeb.Schema.Query.TokenTransfersTest do "amounts" => Enum.map(token_transfer.amounts, &to_string/1), "block_number" => token_transfer.block_number, "log_index" => token_transfer.log_index, - "token_id" => token_transfer.token_id, "token_ids" => Enum.map(token_transfer.token_ids, &to_string/1), "from_address_hash" => to_string(token_transfer.from_address_hash), "to_address_hash" => to_string(token_transfer.to_address_hash), @@ -70,7 +68,6 @@ defmodule BlockScoutWeb.Schema.Query.TokenTransfersTest do amount block_number log_index - token_id from_address_hash to_address_hash token_contract_address_hash diff --git a/apps/block_scout_web/test/block_scout_web/views/api/v2/address_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/api/v2/address_view_test.exs new file mode 100644 index 000000000000..7177316bc125 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/api/v2/address_view_test.exs @@ -0,0 +1,76 @@ +defmodule BlockScoutWeb.API.V2.AddressViewTest do + use BlockScoutWeb.ConnCase, async: true + + import Mox + + alias BlockScoutWeb.API.V2.AddressView + alias Explorer.Repo + + test "for a proxy contract has_methods_read_proxy is true" do + implementation_address = insert(:contract_address) + proxy_address = insert(:contract_address) |> Repo.preload([:token]) + + _proxy_smart_contract = + insert(:smart_contract, + address_hash: proxy_address.hash, + contract_code_md5: "123", + implementation_address_hash: implementation_address.hash + ) + + get_eip1967_implementation_zero_addresses() + + assert AddressView.prepare_address(proxy_address)["has_methods_read_proxy"] == true + end + + def get_eip1967_implementation_zero_addresses do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end +end diff --git a/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs new file mode 100644 index 000000000000..a5aa2e63fa28 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs @@ -0,0 +1,91 @@ +defmodule BlockScoutWeb.API.V2.TransactionViewTest do + use BlockScoutWeb.ConnCase, async: true + + import Mox + + alias BlockScoutWeb.API.V2.TransactionView + alias Explorer.Repo + + describe "decode_logs/2" do + test "doesn't use decoding candidate event with different 2nd, 3d or 4th topic" do + insert(:contract_method, + identifier: Base.decode16!("d20a68b2", case: :lower), + abi: %{ + "name" => "OptionSettled", + "type" => "event", + "inputs" => [ + %{"name" => "accountId", "type" => "uint256", "indexed" => true, "internalType" => "uint256"}, + %{"name" => "option", "type" => "address", "indexed" => false, "internalType" => "address"}, + %{"name" => "subId", "type" => "uint256", "indexed" => false, "internalType" => "uint256"}, + %{"name" => "amount", "type" => "int256", "indexed" => false, "internalType" => "int256"}, + %{"name" => "value", "type" => "int256", "indexed" => false, "internalType" => "int256"} + ], + "anonymous" => false + } + ) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + log1_topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + log2_topic2 = "0x000000000000000000000000000000000000000000000000000000000000634a" + + log1_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + log2_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700000000000000000000000000000000000000000000000000011227ebced227ae00000000000000000000000000000000000000000000001239fdf180a3d6bd85" + + transaction = insert(:transaction) + + log1 = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(log1_topic2), + third_topic: nil, + fourth_topic: nil, + data: log1_data + ) + + log2 = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(log2_topic2), + third_topic: nil, + fourth_topic: nil, + data: log2_data + ) + + logs = [log1, log2] + + assert [ + {:ok, "d20a68b2", + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + [ + {"accountId", "uint256", true, 23833}, + {"option", "address", false, + <<174, 184, 28, 190, 107, 25, 206, 235, 13, 190, 13, 35, 12, 255, 227, 91, 180, 10, 19, 167>>}, + {"subId", "uint256", false, 20_615_843_020_801_704_441_600}, + {"amount", "int256", false, -120_000_000_000_000_000}, + {"value", "int256", false, -522_838_470_013_113_778_446} + ]}, + {:ok, "d20a68b2", + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + [ + {"accountId", "uint256", true, 25418}, + {"option", "address", false, + <<174, 184, 28, 190, 107, 25, 206, 235, 13, 190, 13, 35, 12, 255, 227, 91, 180, 10, 19, 167>>}, + {"subId", "uint256", false, 20_615_843_020_801_704_441_600}, + {"amount", "int256", false, 77_168_037_359_396_782}, + {"value", "int256", false, 336_220_154_890_848_484_741} + ]} + ] = TransactionView.decode_logs(logs, false) + end + end + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end +end diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/helper_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/helper_test.exs index e59a85fb76c4..bcf41d9d4660 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/helper_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/helper_test.exs @@ -27,14 +27,14 @@ defmodule BlockScoutWeb.Tokens.HelperTest do test "returns a string with the token_id with ERC-721 token" do token = build(:token, type: "ERC-721", decimals: nil) - token_transfer = build(:token_transfer, token: token, amount: nil, token_id: 1) + token_transfer = build(:token_transfer, token: token, amount: nil, token_ids: [1], token_type: "ERC-721") assert Helper.token_transfer_amount(token_transfer) == {:ok, :erc721_instance} end test "returns nothing for unknown token's type" do token = build(:token, type: "unknown") - token_transfer = build(:token_transfer, token: token) + token_transfer = build(:token_transfer, token: token, token_type: "unknown") assert Helper.token_transfer_amount(token_transfer) == nil end diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index b92840d3a20c..0c0ba46a2fed 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -29,7 +29,12 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonEdge, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonZkevm, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Beacon, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :manual) Absinthe.Test.prime(BlockScoutWeb.Schema) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 3b67a4dd7f5a..9cb48cea1732 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -39,6 +39,7 @@ defmodule EthereumJSONRPC do Subscription, Transport, Utility.EndpointAvailabilityObserver, + Utility.RangesHelper, Variant } @@ -205,7 +206,8 @@ defmodule EthereumJSONRPC do filtered_params_in_range = filtered_params |> Enum.filter(fn - %{block_quantity: block_quantity} -> is_block_number_in_range?(block_quantity) + %{block_quantity: block_quantity} -> + block_quantity |> quantity_to_integer() |> RangesHelper.traceable_block_number?() end) id_to_params = id_to_params(filtered_params_in_range) @@ -244,7 +246,7 @@ defmodule EthereumJSONRPC do @spec fetch_beneficiaries([block_number], json_rpc_named_arguments) :: {:ok, FetchedBeneficiaries.t()} | {:error, reason :: term} | :ignore def fetch_beneficiaries(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do - filtered_block_numbers = are_block_numbers_in_range?(block_numbers) + filtered_block_numbers = RangesHelper.filter_traceable_block_numbers(block_numbers) Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_beneficiaries( filtered_block_numbers, @@ -277,12 +279,12 @@ defmodule EthereumJSONRPC do @doc """ Fetches blocks by block number list. """ - @spec fetch_blocks_by_numbers([block_number()], json_rpc_named_arguments) :: + @spec fetch_blocks_by_numbers([block_number()], json_rpc_named_arguments, boolean()) :: {:ok, Blocks.t()} | {:error, reason :: term} - def fetch_blocks_by_numbers(block_numbers, json_rpc_named_arguments) do + def fetch_blocks_by_numbers(block_numbers, json_rpc_named_arguments, with_transactions? \\ true) do block_numbers |> Enum.map(fn number -> %{number: number} end) - |> fetch_blocks_by_params(&Block.ByNumber.request/1, json_rpc_named_arguments) + |> fetch_blocks_by_params(&Block.ByNumber.request(&1, with_transactions?), json_rpc_named_arguments) end @doc """ @@ -327,6 +329,16 @@ defmodule EthereumJSONRPC do * `{:error, reason}` - other JSONRPC error. """ + @spec fetch_block_number_by_tag_op_version(tag(), json_rpc_named_arguments) :: + {:ok, non_neg_integer()} | {:error, reason :: :invalid_tag | :not_found | term()} + def fetch_block_number_by_tag_op_version(tag, json_rpc_named_arguments) + when tag in ~w(earliest latest pending safe) do + %{id: 0, tag: tag} + |> Block.ByTag.request() + |> json_rpc(json_rpc_named_arguments) + |> Block.ByTag.number_from_result() + end + @spec fetch_block_number_by_tag(tag(), json_rpc_named_arguments) :: {:ok, non_neg_integer()} | {:error, reason :: :invalid_tag | :not_found | term()} def fetch_block_number_by_tag(tag, json_rpc_named_arguments) when tag in ~w(earliest latest pending safe) do @@ -349,7 +361,7 @@ defmodule EthereumJSONRPC do Fetches internal transactions for entire blocks from variant API. """ def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do - filtered_block_numbers = are_block_numbers_in_range?(block_numbers) + filtered_block_numbers = RangesHelper.filter_traceable_block_numbers(block_numbers) Keyword.fetch!(json_rpc_named_arguments, :variant).fetch_block_internal_transactions( filtered_block_numbers, @@ -357,16 +369,6 @@ defmodule EthereumJSONRPC do ) end - def are_block_numbers_in_range?(block_numbers) do - min_block = Application.get_env(:indexer, :trace_first_block) - max_block = Application.get_env(:indexer, :trace_last_block) - - block_numbers - |> Enum.filter(fn block_number -> - block_number >= min_block && if max_block, do: block_number <= max_block, else: true - end) - end - @doc """ Retrieves traces from variant API. """ @@ -459,25 +461,11 @@ defmodule EthereumJSONRPC do end end - @spec is_block_number_in_range?(quantity) :: boolean() - defp is_block_number_in_range?(block_quantity) do - min_block = Application.get_env(:indexer, :trace_first_block) - max_block = Application.get_env(:indexer, :trace_last_block) - block_number = quantity_to_integer(block_quantity) - - if !block_number || - (block_number && block_number >= min_block && if(max_block, do: block_number <= max_block, else: true)) do - true - else - false - end - end - defp maybe_replace_url(url, _replace_url, EthereumJSONRPC.HTTP), do: url - defp maybe_replace_url(url, replace_url, _), do: EndpointAvailabilityObserver.maybe_replace_url(url, replace_url) + defp maybe_replace_url(url, replace_url, _), do: EndpointAvailabilityObserver.maybe_replace_url(url, replace_url, :ws) defp maybe_inc_error_count(_url, _arguments, EthereumJSONRPC.HTTP), do: :ok - defp maybe_inc_error_count(url, arguments, _), do: EndpointAvailabilityObserver.inc_error_count(url, arguments) + defp maybe_inc_error_count(url, arguments, _), do: EndpointAvailabilityObserver.inc_error_count(url, arguments, :ws) @doc """ Converts `t:quantity/0` to `t:non_neg_integer/0`. @@ -501,11 +489,15 @@ defmodule EthereumJSONRPC do @doc """ Converts `t:non_neg_integer/0` to `t:quantity/0` """ - @spec integer_to_quantity(non_neg_integer) :: quantity + @spec integer_to_quantity(non_neg_integer | binary) :: quantity def integer_to_quantity(integer) when is_integer(integer) and integer >= 0 do "0x" <> Integer.to_string(integer, 16) end + def integer_to_quantity(integer) when is_binary(integer) do + integer + end + @doc """ A request payload for a JSONRPC. """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex index 5737aa909651..85eef70faf75 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex @@ -8,8 +8,34 @@ defmodule EthereumJSONRPC.Block do alias EthereumJSONRPC.{Transactions, Uncles, Withdrawals} + case Application.compile_env(:explorer, :chain_type) do + "rsk" -> + @chain_type_fields quote( + do: [ + bitcoin_merged_mining_header: EthereumJSONRPC.data(), + bitcoin_merged_mining_coinbase_transaction: EthereumJSONRPC.data(), + bitcoin_merged_mining_merkle_proof: EthereumJSONRPC.data(), + hash_for_merged_mining: EthereumJSONRPC.data(), + minimum_gas_price: non_neg_integer() + ] + ) + + "ethereum" -> + @chain_type_fields quote( + do: [ + withdrawals_root: EthereumJSONRPC.hash(), + blob_gas_used: non_neg_integer(), + excess_blob_gas: non_neg_integer() + ] + ) + + _ -> + @chain_type_fields quote(do: []) + end + @type elixir :: %{String.t() => non_neg_integer | DateTime.t() | String.t() | nil} @type params :: %{ + unquote_splicing(@chain_type_fields), difficulty: pos_integer(), extra_data: EthereumJSONRPC.hash(), gas_limit: non_neg_integer(), @@ -29,8 +55,7 @@ defmodule EthereumJSONRPC.Block do total_difficulty: non_neg_integer(), transactions_root: EthereumJSONRPC.hash(), uncles: [EthereumJSONRPC.hash()], - base_fee_per_gas: non_neg_integer(), - withdrawals_root: EthereumJSONRPC.hash() + base_fee_per_gas: non_neg_integer() } @typedoc """ @@ -67,8 +92,22 @@ defmodule EthereumJSONRPC.Block do * `uncles`: `t:list/0` of [uncles](https://bitcoin.stackexchange.com/questions/39329/in-ethereum-what-is-an-uncle-block) `t:EthereumJSONRPC.hash/0`. - * `"baseFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote amount of fee burned per unit gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) - * `"withdrawalsRoot"` - `t:EthereumJSONRPC.hash/0` of the root of the withdrawals. + * `"baseFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote amount of fee burnt per unit gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) + #{case Application.compile_env(:explorer, :chain_type) do + "rsk" -> """ + * `"minimumGasPrice"` - `t:EthereumJSONRPC.quantity/0` of the minimum gas price for this block. + * `"bitcoinMergedMiningHeader"` - `t:EthereumJSONRPC.data/0` of the Bitcoin merged mining header. + * `"bitcoinMergedMiningCoinbaseTransaction"` - `t:EthereumJSONRPC.data/0` of the Bitcoin merged mining coinbase transaction. + * `"bitcoinMergedMiningMerkleProof"` - `t:EthereumJSONRPC.data/0` of the Bitcoin merged mining merkle proof. + * `"hashForMergedMining"` - `t:EthereumJSONRPC.data/0` of the hash for merged mining. + """ + "ethereum" -> """ + * `"withdrawalsRoot"` - `t:EthereumJSONRPC.hash/0` of the root of the withdrawals. + * `"blobGasUsed"` - `t:EthereumJSONRPC.quantity/0` of the total amount of blob gas consumed by the transactions within the block. + * `"excessBlobGas"` - `t:EthereumJSONRPC.quantity/0` of the running total of blob gas consumed in excess of the target, prior to the block. + """ + _ -> "" + end} """ @type t :: %{String.t() => EthereumJSONRPC.data() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | nil} @@ -119,7 +158,22 @@ defmodule EthereumJSONRPC.Block do ...> "timestamp" => Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"), ...> "totalDifficulty" => 340282366920938463463374607431465668165, ...> "transactions" => [], - ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + #{case Application.compile_env(:explorer, :chain_type) do + "rsk" -> """ + "minimumGasPrice" => 345786,\ + "bitcoinMergedMiningHeader" => "0x00006d20ffd048280094a6ea0851d854036aacaa25ee0f23f0040200000000000000000078d2638fe0b4477c54601e6449051afba8228e0a88ff06b0c91f091fd34d5da57487c76402610517372c2fe9",\ + "bitcoinMergedMiningCoinbaseTransaction" => "0x00000000000000805bf0dc9203da49a3b4e3ec913806e43102cc07db991272dc8b7018da57eb5abe59a32d070000ffffffff03449a4d26000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3ad2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a400000000000000000266a24aa21a9ed4ae42ea6dca2687aaed665714bf58b055c4e11f2fb038605930d630b49ad7b9d00000000",\ + "bitcoinMergedMiningMerkleProof" => "0x8e5a4ba74eb4eb2f9ad4cabc2913aeed380a5becf7cd4d513341617efb798002bd83a783c31c66a8a8f6cc56c071c2d471cb610e3dc13054b9d216021d8c7e9112f622564449ebedcedf7d4ccb6fe0ffac861b7ed1446c310813cdf712e1e6add28b1fe1c0ae5e916194ba4f285a9340aba41e91bf847bf31acf37a9623a04a2348a37ab9faa5908122db45596bbc03e9c3644b0d4589471c4ff30fc139f3ba50506e9136fa0df799b487494de3e2b3dec937338f1a2e18da057c1f60590a9723672a4355b9914b1d01af9f582d9e856f6e1744be00f268b0b01d559329f7e0685aa63ffeb7c28486d7462292021d1345cddbf7c920ca34bb7aa4c6cdbe068806e35d0db662e7fcda03cb4d779594638c62a1fdd7ec98d1fb6d240d853958abe57561d9b9d0465cf8b9d6ee3c58b0d8b07d6c4c5d8f348e43fe3c06011b6a0008db4e0b16c77ececc3981f9008201cea5939869d648e59a09bd2094b1196ff61126bffb626153deed2563e1745436247c94a85d2947756b606d67633781c99d7",\ + "hashForMergedMining" => "0xd2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a40",\ + """ + "ethereum" -> """ + "withdrawalsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + "blobGasUsed" => 262144,\ + "excessBlobGas" => 79429632,\ + """ + _ -> "" + end} ...> "uncles" => [] ...> } ...> ) @@ -141,9 +195,24 @@ defmodule EthereumJSONRPC.Block do state_root: "0xc196ad59d867542ef20b29df5f418d07dc7234f4bc3d25260526620b7958a8fb", timestamp: Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"), total_difficulty: 340282366920938463463374607431465668165, - transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: [], - withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + #{case Application.compile_env(:explorer, :chain_type) do + "rsk" -> """ + + bitcoin_merged_mining_coinbase_transaction: "0x00000000000000805bf0dc9203da49a3b4e3ec913806e43102cc07db991272dc8b7018da57eb5abe59a32d070000ffffffff03449a4d26000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3ad2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a400000000000000000266a24aa21a9ed4ae42ea6dca2687aaed665714bf58b055c4e11f2fb038605930d630b49ad7b9d00000000",\ + bitcoin_merged_mining_header: "0x00006d20ffd048280094a6ea0851d854036aacaa25ee0f23f0040200000000000000000078d2638fe0b4477c54601e6449051afba8228e0a88ff06b0c91f091fd34d5da57487c76402610517372c2fe9",\ + bitcoin_merged_mining_merkle_proof: "0x8e5a4ba74eb4eb2f9ad4cabc2913aeed380a5becf7cd4d513341617efb798002bd83a783c31c66a8a8f6cc56c071c2d471cb610e3dc13054b9d216021d8c7e9112f622564449ebedcedf7d4ccb6fe0ffac861b7ed1446c310813cdf712e1e6add28b1fe1c0ae5e916194ba4f285a9340aba41e91bf847bf31acf37a9623a04a2348a37ab9faa5908122db45596bbc03e9c3644b0d4589471c4ff30fc139f3ba50506e9136fa0df799b487494de3e2b3dec937338f1a2e18da057c1f60590a9723672a4355b9914b1d01af9f582d9e856f6e1744be00f268b0b01d559329f7e0685aa63ffeb7c28486d7462292021d1345cddbf7c920ca34bb7aa4c6cdbe068806e35d0db662e7fcda03cb4d779594638c62a1fdd7ec98d1fb6d240d853958abe57561d9b9d0465cf8b9d6ee3c58b0d8b07d6c4c5d8f348e43fe3c06011b6a0008db4e0b16c77ececc3981f9008201cea5939869d648e59a09bd2094b1196ff61126bffb626153deed2563e1745436247c94a85d2947756b606d67633781c99d7",\ + hash_for_merged_mining: "0xd2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a40",\ + minimum_gas_price: 345786,\ + """ + "ethereum" -> """ + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + blob_gas_used: 262144,\ + excess_blob_gas: 79429632,\ + """ + _ -> "" + end} + uncles: [] } [Geth] `elixir` can be converted to params @@ -190,35 +259,54 @@ defmodule EthereumJSONRPC.Block do state_root: "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba", timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), total_difficulty: 1039309006117, - transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: [], - withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + #{case Application.compile_env(:explorer, :chain_type) do + "rsk" -> """ + bitcoin_merged_mining_coinbase_transaction: nil,\ + bitcoin_merged_mining_header: nil,\ + bitcoin_merged_mining_merkle_proof: nil,\ + hash_for_merged_mining: nil,\ + minimum_gas_price: nil,\ + """ + "ethereum" -> """ + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + blob_gas_used: 0,\ + excess_blob_gas: 0,\ + """ + _ -> "" + end} + uncles: [] } - """ @spec elixir_to_params(elixir) :: params - def elixir_to_params( - %{ - "difficulty" => difficulty, - "extraData" => extra_data, - "gasLimit" => gas_limit, - "gasUsed" => gas_used, - "hash" => hash, - "logsBloom" => logs_bloom, - "miner" => miner_hash, - "number" => number, - "parentHash" => parent_hash, - "receiptsRoot" => receipts_root, - "sha3Uncles" => sha3_uncles, - "size" => size, - "stateRoot" => state_root, - "timestamp" => timestamp, - "totalDifficulty" => total_difficulty, - "transactionsRoot" => transactions_root, - "uncles" => uncles, - "baseFeePerGas" => base_fee_per_gas - } = elixir - ) do + def elixir_to_params(elixir) do + elixir + |> do_elixir_to_params() + |> chain_type_fields(elixir) + end + + defp do_elixir_to_params( + %{ + "difficulty" => difficulty, + "extraData" => extra_data, + "gasLimit" => gas_limit, + "gasUsed" => gas_used, + "hash" => hash, + "logsBloom" => logs_bloom, + "miner" => miner_hash, + "number" => number, + "parentHash" => parent_hash, + "receiptsRoot" => receipts_root, + "sha3Uncles" => sha3_uncles, + "size" => size, + "stateRoot" => state_root, + "timestamp" => timestamp, + "totalDifficulty" => total_difficulty, + "transactionsRoot" => transactions_root, + "uncles" => uncles, + "baseFeePerGas" => base_fee_per_gas + } = elixir + ) do %{ difficulty: difficulty, extra_data: extra_data, @@ -239,33 +327,31 @@ defmodule EthereumJSONRPC.Block do total_difficulty: total_difficulty, transactions_root: transactions_root, uncles: uncles, - base_fee_per_gas: base_fee_per_gas, - withdrawals_root: - Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + base_fee_per_gas: base_fee_per_gas } end - def elixir_to_params( - %{ - "difficulty" => difficulty, - "extraData" => extra_data, - "gasLimit" => gas_limit, - "gasUsed" => gas_used, - "hash" => hash, - "logsBloom" => logs_bloom, - "miner" => miner_hash, - "number" => number, - "parentHash" => parent_hash, - "receiptsRoot" => receipts_root, - "sha3Uncles" => sha3_uncles, - "size" => size, - "stateRoot" => state_root, - "timestamp" => timestamp, - "transactionsRoot" => transactions_root, - "uncles" => uncles, - "baseFeePerGas" => base_fee_per_gas - } = elixir - ) do + defp do_elixir_to_params( + %{ + "difficulty" => difficulty, + "extraData" => extra_data, + "gasLimit" => gas_limit, + "gasUsed" => gas_used, + "hash" => hash, + "logsBloom" => logs_bloom, + "miner" => miner_hash, + "number" => number, + "parentHash" => parent_hash, + "receiptsRoot" => receipts_root, + "sha3Uncles" => sha3_uncles, + "size" => size, + "stateRoot" => state_root, + "timestamp" => timestamp, + "transactionsRoot" => transactions_root, + "uncles" => uncles, + "baseFeePerGas" => base_fee_per_gas + } = elixir + ) do %{ difficulty: difficulty, extra_data: extra_data, @@ -285,33 +371,31 @@ defmodule EthereumJSONRPC.Block do timestamp: timestamp, transactions_root: transactions_root, uncles: uncles, - base_fee_per_gas: base_fee_per_gas, - withdrawals_root: - Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + base_fee_per_gas: base_fee_per_gas } end - def elixir_to_params( - %{ - "difficulty" => difficulty, - "extraData" => extra_data, - "gasLimit" => gas_limit, - "gasUsed" => gas_used, - "hash" => hash, - "logsBloom" => logs_bloom, - "miner" => miner_hash, - "number" => number, - "parentHash" => parent_hash, - "receiptsRoot" => receipts_root, - "sha3Uncles" => sha3_uncles, - "size" => size, - "stateRoot" => state_root, - "timestamp" => timestamp, - "totalDifficulty" => total_difficulty, - "transactionsRoot" => transactions_root, - "uncles" => uncles - } = elixir - ) do + defp do_elixir_to_params( + %{ + "difficulty" => difficulty, + "extraData" => extra_data, + "gasLimit" => gas_limit, + "gasUsed" => gas_used, + "hash" => hash, + "logsBloom" => logs_bloom, + "miner" => miner_hash, + "number" => number, + "parentHash" => parent_hash, + "receiptsRoot" => receipts_root, + "sha3Uncles" => sha3_uncles, + "size" => size, + "stateRoot" => state_root, + "timestamp" => timestamp, + "totalDifficulty" => total_difficulty, + "transactionsRoot" => transactions_root, + "uncles" => uncles + } = elixir + ) do %{ difficulty: difficulty, extra_data: extra_data, @@ -331,33 +415,31 @@ defmodule EthereumJSONRPC.Block do timestamp: timestamp, total_difficulty: total_difficulty, transactions_root: transactions_root, - uncles: uncles, - withdrawals_root: - Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + uncles: uncles } end # Geth: a response from eth_getblockbyhash for uncle blocks is without `totalDifficulty` param - def elixir_to_params( - %{ - "difficulty" => difficulty, - "extraData" => extra_data, - "gasLimit" => gas_limit, - "gasUsed" => gas_used, - "hash" => hash, - "logsBloom" => logs_bloom, - "miner" => miner_hash, - "number" => number, - "parentHash" => parent_hash, - "receiptsRoot" => receipts_root, - "sha3Uncles" => sha3_uncles, - "size" => size, - "stateRoot" => state_root, - "timestamp" => timestamp, - "transactionsRoot" => transactions_root, - "uncles" => uncles - } = elixir - ) do + defp do_elixir_to_params( + %{ + "difficulty" => difficulty, + "extraData" => extra_data, + "gasLimit" => gas_limit, + "gasUsed" => gas_used, + "hash" => hash, + "logsBloom" => logs_bloom, + "miner" => miner_hash, + "number" => number, + "parentHash" => parent_hash, + "receiptsRoot" => receipts_root, + "sha3Uncles" => sha3_uncles, + "size" => size, + "stateRoot" => state_root, + "timestamp" => timestamp, + "transactionsRoot" => transactions_root, + "uncles" => uncles + } = elixir + ) do %{ difficulty: difficulty, extra_data: extra_data, @@ -376,12 +458,36 @@ defmodule EthereumJSONRPC.Block do state_root: state_root, timestamp: timestamp, transactions_root: transactions_root, - uncles: uncles, - withdrawals_root: - Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + uncles: uncles } end + defp chain_type_fields(params, elixir) do + case Application.get_env(:explorer, :chain_type) do + "rsk" -> + params + |> Map.merge(%{ + minimum_gas_price: Map.get(elixir, "minimumGasPrice"), + bitcoin_merged_mining_header: Map.get(elixir, "bitcoinMergedMiningHeader"), + bitcoin_merged_mining_coinbase_transaction: Map.get(elixir, "bitcoinMergedMiningCoinbaseTransaction"), + bitcoin_merged_mining_merkle_proof: Map.get(elixir, "bitcoinMergedMiningMerkleProof"), + hash_for_merged_mining: Map.get(elixir, "hashForMergedMining") + }) + + "ethereum" -> + params + |> Map.merge(%{ + withdrawals_root: + Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + blob_gas_used: Map.get(elixir, "blobGasUsed", 0), + excess_blob_gas: Map.get(elixir, "excessBlobGas", 0) + }) + + _ -> + params + end + end + @doc """ Get `t:EthereumJSONRPC.Transactions.elixir/0` from `t:elixir/0` @@ -685,7 +791,7 @@ defmodule EthereumJSONRPC.Block do end defp entry_to_elixir({key, quantity}, _block) - when key in ~w(difficulty gasLimit gasUsed minimumGasPrice baseFeePerGas number size cumulativeDifficulty totalDifficulty paidFees) and + when key in ~w(difficulty gasLimit gasUsed minimumGasPrice baseFeePerGas number size cumulativeDifficulty totalDifficulty paidFees minimumGasPrice blobGasUsed excessBlobGas) and not is_nil(quantity) do {key, quantity_to_integer(quantity)} end @@ -700,15 +806,15 @@ defmodule EthereumJSONRPC.Block do # hash format defp entry_to_elixir({key, _} = entry, _block) when key in ~w(author extraData hash logsBloom miner mixHash nonce parentHash receiptsRoot sealFields sha3Uncles - signature stateRoot step transactionsRoot uncles withdrawalsRoot), + signature stateRoot step transactionsRoot uncles withdrawalsRoot bitcoinMergedMiningHeader bitcoinMergedMiningCoinbaseTransaction bitcoinMergedMiningMerkleProof hashForMergedMining), do: entry defp entry_to_elixir({"timestamp" = key, timestamp}, _block) do {key, timestamp_to_datetime(timestamp)} end - defp entry_to_elixir({"transactions" = key, transactions}, _block) do - {key, Transactions.to_elixir(transactions)} + defp entry_to_elixir({"transactions" = key, transactions}, %{"timestamp" => block_timestamp}) do + {key, Transactions.to_elixir(transactions, timestamp_to_datetime(block_timestamp))} end defp entry_to_elixir({"withdrawals" = key, nil}, _block) do diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block/by_hash.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block/by_hash.ex index 07d1e48b4d1e..5a923a3a95b1 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block/by_hash.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block/by_hash.ex @@ -3,9 +3,7 @@ defmodule EthereumJSONRPC.Block.ByHash do Block format as returned by [`eth_getBlockByHash`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash) """ - @include_transactions true - - def request(%{id: id, hash: hash}) do - EthereumJSONRPC.request(%{id: id, method: "eth_getBlockByHash", params: [hash, @include_transactions]}) + def request(%{id: id, hash: hash}, hydrated \\ true) do + EthereumJSONRPC.request(%{id: id, method: "eth_getBlockByHash", params: [hash, hydrated]}) end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex index a501a16bc817..76ffbe6b5cc4 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex @@ -115,9 +115,23 @@ defmodule EthereumJSONRPC.Blocks do state_root: "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", timestamp: Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"), total_difficulty: 131072, - transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"], - withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + #{case Application.compile_env(:explorer, :chain_type) do + "rsk" -> """ + bitcoin_merged_mining_coinbase_transaction: nil,\ + bitcoin_merged_mining_header: nil,\ + bitcoin_merged_mining_merkle_proof: nil,\ + hash_for_merged_mining: nil,\ + minimum_gas_price: nil,\ + """ + "ethereum" -> """ + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",\ + blob_gas_used: 0,\ + excess_blob_gas: 0,\ + """ + _ -> "" + end} + uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"] } ] diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex index 729b218200ae..64ed49911d59 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex @@ -133,9 +133,13 @@ defmodule EthereumJSONRPC.Contract do defp convert_int_string_to_array_inner(arg) do arg - |> Enum.map(fn el -> - {int, _} = Integer.parse(el) - int + |> Enum.map(fn + el when is_integer(el) -> + el + + el -> + {int, _} = Integer.parse(el) + int end) end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex index 37573679aa51..1b6feee0adbc 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex @@ -13,7 +13,7 @@ defmodule EthereumJSONRPC.Encoder do """ @spec encode_function_call(ABI.FunctionSelector.t(), [term()]) :: String.t() def encode_function_call(function_selector, args) when is_list(args) do - parsed_args = parse_args(args) + parsed_args = parse_args(args, function_selector.types) encoded_args = function_selector @@ -25,16 +25,26 @@ defmodule EthereumJSONRPC.Encoder do def encode_function_call(function_selector, args), do: encode_function_call(function_selector, [args]) - defp parse_args(args) when is_list(args) do + defp parse_args(args, {:array, type}) when is_list(args) do + Enum.map(args, fn arg -> parse_args(arg, type) end) + end + + defp parse_args(args, types) when is_list(args) do args - |> Enum.map(&parse_args/1) + |> Enum.zip(types) + |> Enum.map(fn {arg, type} -> + parse_args(arg, type) + end) end - defp parse_args(<<"0x", hexadecimal_digits::binary>>), do: Base.decode16!(hexadecimal_digits, case: :mixed) + defp parse_args(<>, type) when type in [:string, "string"], + do: hexadecimal_digits |> Base.encode16() |> try_to_decode() + + defp parse_args(<<"0x", hexadecimal_digits::binary>>, _type), do: Base.decode16!(hexadecimal_digits, case: :mixed) - defp parse_args(<>), do: try_to_decode(hexadecimal_digits) + defp parse_args(<>, _type), do: try_to_decode(hexadecimal_digits) - defp parse_args(arg), do: arg + defp parse_args(arg, _type), do: arg defp try_to_decode(hexadecimal_digits) do case Base.decode16(hexadecimal_digits, case: :mixed) do diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/filecoin.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/filecoin.ex new file mode 100644 index 000000000000..4afbd33c6cee --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/filecoin.ex @@ -0,0 +1,5679 @@ +defmodule EthereumJSONRPC.Filecoin do + @moduledoc """ + Ethereum JSONRPC methods that are only supported by Filecoin. + + Sample response from FEVM `trace_block` method: + + curl -s -X POST \ + -H "Content-Type: application/json" \ + --data '{"method":"trace_block","params":["0x37E611"],"id":1,"jsonrpc":"2.0"}' \ + http://...:1234/rpc/v1 | jq -r .result + [ + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000021cc23", + "to": "0xff000000000000000000000000000000001a34e5", + "gas": "0x1891a7d", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850d8182004081820d58c0960ee115a7a4b6f2fd36a83da26c608d49e4160a3737655d0f637b81be81b018539809d35519b0b75ca06304b3b4d40c810e50b954e82c5119a8b4a64c3e762a7ae8a2d465d1cd5bf096c87c56ab0da879568378e5a2368c902eea9898cf1e2a1974ddb479ec6257b69aca7734d3b3e1e70428c77f9e528ffcb3dc3f050f0193c2cc005927a765c39a4931d67fb29aaba6e99f2c7d2566b98fdbf30d6e15a2bbd63b8fa059cfad231ccba1d8964542b50419eaad4bc442d3a1dc1f41941944c11a0037e5f45820d41114bb6abbf966c2528f5705447a53ee37b7055cd4478503ea5eaf1fe165c60000000000000000000000000000" + }, + "result": { + "gasUsed": "0x14696c1", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf37d8b8bf67df3ddaa264e22322d2b092e390ed33f1ab14c8a136b2767979254", + "transactionPosition": 1 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000012e66c", + "to": "0xff0000000000000000000000000000000012e5f7", + "gas": "0x4482939", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285108182064081820d58c097ea8a30fc450e9f5370bdfd0a5fbadb528b137c52ba4b22cbdd91cd1b312707556314b8400967aeb858c2dc8d68d7eb8b76960a414bf41ad73831bd6500d3ff06d8f8b1af823e1d2f5f9803d5402c4038b87a4c77803589bc0a9b982ae90d4a02381370e0f4aa4f3145acaa5a99854ba6bffbf02778c2f7ed66b141da1aab9fac560a184662c5e47e2764e9c4221ff982c750a5aafb97968a0348331218b069e0f754e62341ed115f2f05a5c86def9ce1dff851918cfa69095611517d99f27e1a0037e5f358205ded7c8109656ee788ec8de051bc70331254b551d154eac21abf8fcf339d86b50000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3c65351", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x83ca0e80894733453286b03e4caa9b1f3d4f4e14e52e583a46c99f3504a10e78", + "transactionPosition": 2 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000012e66c", + "to": "0xff0000000000000000000000000000000012e5f7", + "gas": "0x42d9aa2", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285108182074081820d58c08a894f371e2e6808da865600757651857a086c1b186f6b8b7e28ad730a0d4febc506609da7629fd39966a7d44ca8d40c8203bf0625b54f4dc6a5598fc5154a0498e940820d49b5c19fa1211766feac30d08f2f886be3e3e677d6da346b9eb92a0a89aaeb839f00ad85631801e18397c1390a3847d3b9fd04f091f55ba561ebe401d6d66baa19e41fb7aa030590c5808280431c0b0d6a64fb2dbc77f3e79ddd563b039dcc821b30bbf8d55863066f48c8fb3c1e8504754a77238eb65ce35e309c1a0037e5f358205ded7c8109656ee788ec8de051bc70331254b551d154eac21abf8fcf339d86b50000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3ba34f2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x4e0ddcc7723b6adb8b04553005bf7d2b92ff21515a2d475ed7cd904adf463ea2", + "transactionPosition": 3 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000012e66c", + "to": "0xff0000000000000000000000000000000012e5f7", + "gas": "0x37e0b4f", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285108182084081820d58c0a621f85fb8c62ff3dfc46ceb3c8b39eaf26d12058084cf132e8b929e76d78b09f8dcab75ae6b8fb0c08cbb19401282b8a22a134dd6501245016c6b291475ffd9f5c849472158d35a4a2fbb1c3ae3a9659c6977bbba744ca93d88ce6b8463c2240c76e0369b0b7ce704c125ee0a1659bbe480f9332d93ce5cdfc8a4165a375faca2f7dd6f32b1ad5e7139a67132abc88fb92087b5bbfc783c538b72f940ff1270670a8d4cf75c00f841f7428a4882fc81ba7e878f130d4d064c32b297db1d83b61a0037e5f358205ded7c8109656ee788ec8de051bc70331254b551d154eac21abf8fcf339d86b50000000000000000000000000000" + }, + "result": { + "gasUsed": "0x32c5c44", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0ff1f5cf62ea3d5bff02b2179b65e04351887c8818c674566b97c40f00bb31cb", + "transactionPosition": 4 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000012e66c", + "to": "0xff0000000000000000000000000000000012e5f7", + "gas": "0x373949a", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285108182094081820d58c0894b04554ba37a92ed433153a56ef1252a8c9b03c959eb389de91e623f8ed365dde113420eedc91ecc4e3ddd6b7769ec9300462f93ca58128694b0e7479ef0e055cfb5852f19e3cf682ba5ef3508b42e25f9b3fc8b3eba3b49d8343d5e366a3500a8f48a186b76cc22b4caf1496d209daacd2bc310de177f820c9acf354bc12d26f90f36a18a63c90d7a8857e6c0c608b63488ac62d688931752d4664331b1445aaf9ee9b2d394e48282f89e1eeef729617a097817ee1aa62a2cf5219f9d02df1a0037e5f358205ded7c8109656ee788ec8de051bc70331254b551d154eac21abf8fcf339d86b50000000000000000000000000000" + }, + "result": { + "gasUsed": "0x344d3f5", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x037fde0b4fd04ee19c25d3ede9e65a37726995e3b11e8d475035b26db230782d", + "transactionPosition": 5 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000012e66c", + "to": "0xff0000000000000000000000000000000012e5f7", + "gas": "0x3780216", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2851081820a4081820d58c087803bf7ff2604290afb95c3e1084540ead1f85b60816b716253e741d9637f1521c4dc70f6b3a53f593104e7d063873ba924e36be13b16f6bcc594199faf091c36c65bc971026e24af1ee7b20aeec80a3cc4edf7444bb1ae5943ff4cb01027fb101199f22ef757fbe6e7ef13560b7d0558fe5270cd529907d91c223f61a31096dab63246b7717cd517b8d68744c193f68b60d96b1f4eaf7761e87ceeea4fc378c1709b29e19d762f50ee1b8337e5f96135c35837a2d858b66116d0e1eb2800541a0037e5f358205ded7c8109656ee788ec8de051bc70331254b551d154eac21abf8fcf339d86b50000000000000000000000000000" + }, + "result": { + "gasUsed": "0x322a428", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x98021260660c66e6be8af4b99f97e941b6ddcb2c48e84d2d0ad338522159a706", + "transactionPosition": 6 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000012e66c", + "to": "0xff0000000000000000000000000000000012e5f7", + "gas": "0x374cedf", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2851081820b4081820d58c0b6f12b90e50d2c0b86f2cc341a1e8a7787c2e78b933f4613a7af22cdfb66ce15a53bcd9f79092ecd91d0677310f86791b3142770e01ecf3a9eeab55ed5294435d108773019404632105da406f9558eca8bf25295c0a6d5885c95f24e65050e7203c7d40e07a30103ef93233b137908ef4ea3fc3d0741c0bf1c3f439ec4b23fbff6be77e190cf2ba6f7bd609b10055ffb931645123bf6d85ead1fa04e6800717ff253eacf5d3bb9ccc8afadada897aae831aabfc20cabed2ff25d0a24f37a3c501a0037e5f358205ded7c8109656ee788ec8de051bc70331254b551d154eac21abf8fcf339d86b50000000000000000000000000000" + }, + "result": { + "gasUsed": "0x32ae600", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd3989d5f90ec878cc091194075bb9ed447ccae35a8dcc4035fad9c44987c55fd", + "transactionPosition": 7 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000023c695", + "to": "0xff000000000000000000000000000000001d922c", + "gas": "0x1a964d9", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285148182004081820e58c0a24cb66ff00fd1f20a84a596a215dc56f44b240eb7c7fa2a9cc1f103a86388ee860b5ba5e6cba7cd8869b194c1b45fc292ae448fc49a96f16b891ce29aa6560f36273080297214c94845cb210144e1a237a82e427eda1bbcf75bb8fa914134c118c6687cf61f857e92a1f19cb6547235ac0802384e5e6b5fbdcf81dfa3b8a60aa92ffd452ff362aee38dacd297c611f3b5de12f612b38a366f3353274f784d820956afd67eb365d4cd53af1140ed2bed9bcbcd31da2b58b83651994b3bc694451a0037e60358202f916e2c95962eb5027d2f5fc467e9983027e6da80714e1b7169cda64ff2bb610000000000000000000000000000" + }, + "result": { + "gasUsed": "0x17effb4", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xcee1106b264da4388f0b82d3eb834d75a5c82bd5baa76f58f56dd2d4df8f118b", + "transactionPosition": 8 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002af64b", + "to": "0xff000000000000000000000000000000002af684", + "gas": "0x490a329", + "value": "0x1734adf7a686149e", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a0002997259078097f66fc0113af242cae4bb45a9a217eb538185b0b3cdcf059af23b77984e794d0dcb1994ca805f5621818d80b192ac269665ef565bcf33e86568c0b2ce805875dd607cf8875725c94646f87adc591a1d1bcdad9e5defe0474898ecc9178e50a709f7a49c4d41de880fa7dccaefecd9c4c499fdf84eac1446e23b9c746d2f929afce10a9fe4085d7f9af6a883e9bbe85995e07b2277e58a4bf8d106490a989c0d7fba52162a15d5d1de3580238ca8bd84c341fd5e3dd90e58604b44e9e6efa26f8bdb01f81f72e8a90e4d3781e6ee575ced52406e111c29cf8d39d394079ac4678c907b2e9b2aee659ea7f1862cb73805ab42c3f7962a6c63e43d5615da1fa4620c5accb67f30463512afe1ea313367efabc66d132eca27dd4edc5a4f88cabe6517ef84269d1db59b0a9b5df61babf9f4e60f2a9ebd6bb2f1cf4c51fb3e899a64866b05775a76d9d02835c7c72dbe9c1aae0eba486b759688b26bcf3dbb8a56b99fa90219b4b72ddf96c198bf37309c0e87f7bbcc4207397efd93176508b1948b83b9d29e6fb9eb90f175c634374e662ca6618c27b4044caab2682b295e490bfaa3816b6856ba95b07c88a4d354f66526b6bcd48cb7b3dbe34651c7ed4fe0522a802c4abd8c9865bed859706476ef28e65e0ef995f6ed4355cd2ad049b28b479218a2d145c48484b15997e1e50e0e811bb25da5e299970d83cc12e927261f4672444f24a7181364dd2c0a46f38aa24bd38284398e866b5012d763ee70fa8c6851c28083cc47a3ce35712dfb578f99c2cb878f0eab25e380dc88ee6ecb705b31cb9492c01937ef66e226efc048dd5ae78cacf8391056d4d0555bfb37ee93865b5d79d73609b3b80c5c22fb22826834e81699a29fe7667d577fb4b1719fbbdfb988afe34d91b009a0c92f6a9f33f04e4237a5b5db7124d46b80d4af0592e338790008841c0d1562229a78bfa2e0a084e4144aed4edac7a8b7504848979452ad547909552382b655009bc4257d96f0f3fe39960fe3ea1a5c6ccd0d5bf09462f53b8bb298abfd5c5a039c7bf4d76f908574827ff275e129a29f057b3410d61d35788db2eadfc5b32ae50b3169df0bb625ca5a88dda75e0e4d5bf16b669df3ce3e3abd89bd5369ab8fae5fc4fef3b427f991ab800c9207df5028ceec134b6d65b62ee8feca0014c1a5bb13473bef7c64589df5300c1ce2b9bf52a4b296a21f7e64506a0d1dc5fcd208e71cb66df368346857546e886bf6961534d4208c312a415c46951fde39ae9fe2716fb76f93a654b260a584339bf04f08e92b09364de4079533fc5685a0379bd07a63c8d8c0d5150ccd54575138fcdcfc386ecca7e176f0369b7f830271d00f756227bf1858ff0c4b0a39db2586f81f1c2eb94e6dd800c41604a7daa5893f880fd0ec02196e5fed08f2f7837de91e9ab414479e220bb734a20081ff3709d623548efd15fa09dcb976aee94e61e54261b074f4e43a88ece20d167a0c8a255ca5da4197cef5bcaff55f80e7a4016a2bddcd9a772ea3a5b311041c4912087260c7028b17076a3a580da693eb98b25cc3ac3384cfeca799f185fadbe4a24a87d40e675ba2324870fe946d28c38bd385a8e75dafcb7c17209fc6ab477196a9d0b8c1cd853a41a6a99431114cb391a62449fc2b9a17149c321ed7937cb6a5f534fdc6cfc0ba7b96e0f7c9141e49905c40df94acbc1375f8d6d04101dd3b2aa031b68c27f50158879e705de80672abd7708f88d3446cb19261533a34e26a17ffa04395e3d130b13a96081f1d9c06a235168c6c81d471aaad92d5a9ee4b9f02822ac53d3ef5ce2020f47bed59d05999c3916325105c6a052870f678fe52019150bbafaf4f06db0bfc6f1f3461fc205e84a3232bd885ef2b7baf7e635e4730b29631c723318c7c37671d30c26e967804aa34c94eb927c868c0eabbe394262c18699d7275a735f2a5e5191c64dccd03986556e7f541475bcec57a8b43baab5f915168872d46d5a3292db6da12f75eb57ca579f8bc4dd9ab206cb5fe7cdf3031135515d8d01802fd78ad24ef1fc91814e010620e8a6eec9f7c0fd5efbee822072dac9f21965fde15fde86b25eabc02818a2e92d997c4ea0b2903bf0a7d3ec24754f6105511f72880810133487b02754bad91d57a342fc9844077b0b082158d728b2c3bc0ec07ca096f2996eeb83e3b45934a76d8e98b9afd8f11899aa15b374ce63108375fd93386ae4e4e0b0d6c22d4acfa0983faddeee59869e54919ee7a52a3fb87e528ed6b63172a647974534f957808b0720656dc5b963a2111edab5a731559a8935397c786869d96f60a1a5c0c4ad57084262a2b33139efa864eeff0d350327903b1db5ac981e6603160b47308b9570a8d0a0a33816b7b35dca64d40b9316ab9bb9d29b10fc09072235758da48817ac298870691e245b099cc7a7298f490e46609a8de673cc645d4dadda47a13291bf52c7eceeed22bc18c522a9063c81fae43eb13f04f1473421d784f05b7f9a595a053fa3be4fe7efba02cc23d4f4c1111bda71774695176c79c904f1d7863044004055200cef7c76e62d9902118cf063dd599b0d30824074e0306cc3d01f18d68aa46aa986e3146847e0e23289fbc9e30e13a54d39885bc65de72eb7a7756a44bc6effe2584f1a8ffba854295e4d98d3ae515a551538aded3ed59e034b9c24ab3e7792aecdb84c7e6443bc5f6932e0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x773e01", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x4d88a10bcf487e66019cae5da1f3e29fd36de5ebeabe342d034b0dcc70bf32f4", + "transactionPosition": 9 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002af684", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x44a899c", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a002af6841a00029972811a045b2b535820c4602a2df716d569dd747e0fdb722f40fa6ee1a9fca69c250bd098de14a9b3ff58203ea65ad0e910f3d9e0556bfe40f97e34cf87a3307197b1f9fedc51dc291f828859078097f66fc0113af242cae4bb45a9a217eb538185b0b3cdcf059af23b77984e794d0dcb1994ca805f5621818d80b192ac269665ef565bcf33e86568c0b2ce805875dd607cf8875725c94646f87adc591a1d1bcdad9e5defe0474898ecc9178e50a709f7a49c4d41de880fa7dccaefecd9c4c499fdf84eac1446e23b9c746d2f929afce10a9fe4085d7f9af6a883e9bbe85995e07b2277e58a4bf8d106490a989c0d7fba52162a15d5d1de3580238ca8bd84c341fd5e3dd90e58604b44e9e6efa26f8bdb01f81f72e8a90e4d3781e6ee575ced52406e111c29cf8d39d394079ac4678c907b2e9b2aee659ea7f1862cb73805ab42c3f7962a6c63e43d5615da1fa4620c5accb67f30463512afe1ea313367efabc66d132eca27dd4edc5a4f88cabe6517ef84269d1db59b0a9b5df61babf9f4e60f2a9ebd6bb2f1cf4c51fb3e899a64866b05775a76d9d02835c7c72dbe9c1aae0eba486b759688b26bcf3dbb8a56b99fa90219b4b72ddf96c198bf37309c0e87f7bbcc4207397efd93176508b1948b83b9d29e6fb9eb90f175c634374e662ca6618c27b4044caab2682b295e490bfaa3816b6856ba95b07c88a4d354f66526b6bcd48cb7b3dbe34651c7ed4fe0522a802c4abd8c9865bed859706476ef28e65e0ef995f6ed4355cd2ad049b28b479218a2d145c48484b15997e1e50e0e811bb25da5e299970d83cc12e927261f4672444f24a7181364dd2c0a46f38aa24bd38284398e866b5012d763ee70fa8c6851c28083cc47a3ce35712dfb578f99c2cb878f0eab25e380dc88ee6ecb705b31cb9492c01937ef66e226efc048dd5ae78cacf8391056d4d0555bfb37ee93865b5d79d73609b3b80c5c22fb22826834e81699a29fe7667d577fb4b1719fbbdfb988afe34d91b009a0c92f6a9f33f04e4237a5b5db7124d46b80d4af0592e338790008841c0d1562229a78bfa2e0a084e4144aed4edac7a8b7504848979452ad547909552382b655009bc4257d96f0f3fe39960fe3ea1a5c6ccd0d5bf09462f53b8bb298abfd5c5a039c7bf4d76f908574827ff275e129a29f057b3410d61d35788db2eadfc5b32ae50b3169df0bb625ca5a88dda75e0e4d5bf16b669df3ce3e3abd89bd5369ab8fae5fc4fef3b427f991ab800c9207df5028ceec134b6d65b62ee8feca0014c1a5bb13473bef7c64589df5300c1ce2b9bf52a4b296a21f7e64506a0d1dc5fcd208e71cb66df368346857546e886bf6961534d4208c312a415c46951fde39ae9fe2716fb76f93a654b260a584339bf04f08e92b09364de4079533fc5685a0379bd07a63c8d8c0d5150ccd54575138fcdcfc386ecca7e176f0369b7f830271d00f756227bf1858ff0c4b0a39db2586f81f1c2eb94e6dd800c41604a7daa5893f880fd0ec02196e5fed08f2f7837de91e9ab414479e220bb734a20081ff3709d623548efd15fa09dcb976aee94e61e54261b074f4e43a88ece20d167a0c8a255ca5da4197cef5bcaff55f80e7a4016a2bddcd9a772ea3a5b311041c4912087260c7028b17076a3a580da693eb98b25cc3ac3384cfeca799f185fadbe4a24a87d40e675ba2324870fe946d28c38bd385a8e75dafcb7c17209fc6ab477196a9d0b8c1cd853a41a6a99431114cb391a62449fc2b9a17149c321ed7937cb6a5f534fdc6cfc0ba7b96e0f7c9141e49905c40df94acbc1375f8d6d04101dd3b2aa031b68c27f50158879e705de80672abd7708f88d3446cb19261533a34e26a17ffa04395e3d130b13a96081f1d9c06a235168c6c81d471aaad92d5a9ee4b9f02822ac53d3ef5ce2020f47bed59d05999c3916325105c6a052870f678fe52019150bbafaf4f06db0bfc6f1f3461fc205e84a3232bd885ef2b7baf7e635e4730b29631c723318c7c37671d30c26e967804aa34c94eb927c868c0eabbe394262c18699d7275a735f2a5e5191c64dccd03986556e7f541475bcec57a8b43baab5f915168872d46d5a3292db6da12f75eb57ca579f8bc4dd9ab206cb5fe7cdf3031135515d8d01802fd78ad24ef1fc91814e010620e8a6eec9f7c0fd5efbee822072dac9f21965fde15fde86b25eabc02818a2e92d997c4ea0b2903bf0a7d3ec24754f6105511f72880810133487b02754bad91d57a342fc9844077b0b082158d728b2c3bc0ec07ca096f2996eeb83e3b45934a76d8e98b9afd8f11899aa15b374ce63108375fd93386ae4e4e0b0d6c22d4acfa0983faddeee59869e54919ee7a52a3fb87e528ed6b63172a647974534f957808b0720656dc5b963a2111edab5a731559a8935397c786869d96f60a1a5c0c4ad57084262a2b33139efa864eeff0d350327903b1db5ac981e6603160b47308b9570a8d0a0a33816b7b35dca64d40b9316ab9bb9d29b10fc09072235758da48817ac298870691e245b099cc7a7298f490e46609a8de673cc645d4dadda47a13291bf52c7eceeed22bc18c522a9063c81fae43eb13f04f1473421d784f05b7f9a595a053fa3be4fe7efba02cc23d4f4c1111bda71774695176c79c904f1d7863044004055200cef7c76e62d9902118cf063dd599b0d30824074e0306cc3d01f18d68aa46aa986e3146847e0e23289fbc9e30e13a54d39885bc65de72eb7a7756a44bc6effe2584f1a8ffba854295e4d98d3ae515a551538aded3ed59e034b9c24ab3e7792aecdb84c7e6443bc5f6932ed82a5829000182e20381e8022036ea0ddccbc309b556e4fdc791438210c26b8b03c39d18834ba80a36bbdd2d6cd82a5828000181e203922020487c0c209649065502b78229fc0b7615d57ece831e64ce43bd3d8c9c42a8c72b00000000000000000000000000" + }, + "result": { + "gasUsed": "0x2ddb87d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x4d88a10bcf487e66019cae5da1f3e29fd36de5ebeabe342d034b0dcc70bf32f4", + "transactionPosition": 9 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000011edcb", + "to": "0xff0000000000000000000000000000000011edd8", + "gas": "0x1a9c56b", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850d8182004081820d58c0a319a243963a781b9065dd3cc6cb01dd443611f71e95c03b2c43067837920f7c0091d21220384a8d9fbc2b7485aece108ff8910a39017f80753b3dc546957d44fd949fa40facd72ff1f62b3d20dfadfb1d6b5bb0716284bfd4e0b80078039d500e43ccd19408e9f8f2e355df930feabe48aa2d27426bc1f5f9a3dfc08b1f7ab1d699abf7d86811e82e261575be40bdac8bbec5b061f3bdcbca100996367c6e4e33a7aed7ab5f4d9543a5a69f89ad068a150a29e0aacbed0b50803956fdf161531a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x16185c7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0ce7f1628f49ca1f4714588d59b6c3fc7791441a7833af66e373be1c4064b926", + "transactionPosition": 10 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001e4c50", + "to": "0xff000000000000000000000000000000001e4c4b", + "gas": "0x1a4fb4d", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850b8182004081820d58c086ecedea35396878a90a05fd41b2ed46c73869919a9c07fe1ad04d67f97b9bb0040c70eb525811ef34bc95c4664979ba887ac5fe5539cefa9ad5d572cbe31833dd735eae1e3afd27982c2058e68ef536fa96211b0a236edc47904dd9e6028f95101167d96f9b7ca02324ed4f51120454c6be09703b0ce0d6d66e6150f1c11544b2a3ba052eef2233fd8c99d0818754efacfd3fde07189a9b927073627d23177dbb1908fbc60ec768af3b957833df277f702e08605456646890744f16fdc256231a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x15db0e5", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x12dbb560d3620454a40e275965817a7fe23c82e2e6870a32261d1e1469e7694e", + "transactionPosition": 11 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001b6165", + "to": "0xff000000000000000000000000000000001134fe", + "gas": "0x304eec29", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f9851381820047e03a7420ed060181820d58c0ac75f6bdc5cef0564f0d17c739ef653c268ab69c15398de184d3a078f758a7948b81092f68636a6932ce7a13bb8a7ce38910b7287ac053bc9b33c33dfae64e5488103cd61842f1436f844cd71f67d69ef5fefefdcf32d9c759a0b63e878381f009e3b8a1c77ff44855979bec458688aba0538f793866d0736224f55e689597207c315007cccd3fdecba298b46b59a364ace16c261151728065e32a59505eafda71866382dea9138bda682ac474030a49effab33f17a5119a8181b3a416737c931a0037e5e758205d5641b3010b198d37a1939dc3e78c500a46a23f433f5c1e5cb8b8847ce653d900000000000000" + }, + "result": { + "gasUsed": "0x25482600", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x333b9ec6232ca0b62db161908423e51e9c15847dc1af46bea47ecfbc5c9a1de4", + "transactionPosition": 12 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001134fe", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0xb1b85b3", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f8246010800000000460150000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1699cec", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x333b9ec6232ca0b62db161908423e51e9c15847dc1af46bea47ecfbc5c9a1de4", + "transactionPosition": 12 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001258fe", + "to": "0xff00000000000000000000000000000000110330", + "gas": "0x1b7e49d", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285028182004081820d58c0abdbdc0a9b0148ce1f4cc7774bbf9f8fb55359a26601885ac33ed35e76777a4bcfc34970fcd77ddbcae1eb1f7482d886aa811dbb853217324c2e3883c72dc559aa75e59c266ca217c44d1bfe2d7454a35c6b58a66a46e1e8ecde020d596e3c5b18b5c4111bcc5a02a840b5eaef79804cb8495df887488c77ca3d39aac9fb2e8d6029075253f8574896e98a6467aecb12b36223ecd87ba86489b351e004dd803e701a5545df4f04b499894f564240b5b812b84d4a76171e22153dff71d0c076b91a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x16c053d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x78c49827be7cb10a97def7625f879f784e74d319f0163e41a0bfb9e69bd50a23", + "transactionPosition": 13 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3cd0", + "to": "0xff000000000000000000000000000000002b3dda", + "gas": "0x2c685ac", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b685128282004082014081820d590180b06714322c16170ecb7c9c0368bca79c52e042c91842f054d7ff485b6f0eebe6eb842984fc8fb422f6aeee8014dd9318889569e50128dd7836a3a772b3cb42b74b5683bf14f0f6f2408365594f66c0dc2b9892240e09288bbc5c581edda19f4d01e95a51180cd10582f7c98a65b0eed7285e99f8a0ac9f9da0d65ae7a9fbd0c15ca17feb4707418f8f290079e9e0e7fb9149a2a00ff464b45b456962ab56bb3c22f0385cda8d34487df04e7a8f97e5bd1afa7fa79e6057ae32dd840c2b65308da9c82b53bbaef4e6ec4ca0a0ed87f48d69232ec9ed7dfdc27ec802ea31bed16f936bc6c3e6a1bd7a9bf866065eb33ce2b982e2138648025932a9f5d44a4e164ebea35a41267a0ba86be5ed21764d0f120275658d1480be047ded3fd6f1e5f7f611b774515ff8669dfd229401058548a21ebdfdc5f9e2ac6a045d6b4c9e095f37d2ae66d7e7b44258a47aca8cada74470a2efbd48c33cb04479ffe5cf1c0a2251ad6930a75b7381ee9fbce76e1bb149dc72a72f9e7ac0c7b66f9c4080e32703661a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000" + }, + "result": { + "gasUsed": "0x248721c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc7602fdaef4828a493b88f871dcf5c5750270f84c69f609c4ed047b30e49ab21", + "transactionPosition": 14 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b42c9", + "to": "0xff000000000000000000000000000000002b2288", + "gas": "0x1919d9a", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285018182004081820d58c09315579871183fc3376dbacee3f035e3740908c1c2dff5930be1c84897435c1fdb3a3381fc674ad538ae651328dfbae2adaf6cc61cb8677c27c0fa6bd966544735584e3b2417df9697ec1af32e07a53c68be0dba944b20a0df6d66a1ee74af6606460b398e6a5e76c7aefb8426b60a89014c62db52ae4b946bffd71ebc66d9874829e31f1777c487a86f64cc785e1d9f91d5d367a76a699b368ad9cb21da81bbe3ca211498d38a4d3669b33b1f8dc7404fdb2b87db35f82482819509e38dd70d1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x14e3bdf", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x32042ed62912073406b1f825b0d4baf5256b6773cfcab6c269352c74f6b7c39d", + "transactionPosition": 15 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000129602", + "to": "0xff0000000000000000000000000000000012968a", + "gas": "0x219f358", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f38518258182024081820d58c0a4c24df9a93858d253c0b949ab0cfd5043f8243151ae45eaaa8836e1d5c7668fe6d88c1b8ea0d172543c781f5cb35fe8b4b97f5cd4495bcc3d60c4d475707afd9ecc87dd4abe324c80c86c8cbe557455b5d67f473018cbd37fe02a95e4603d740c2f2e281124b9ce62ec8279a9b8c0cf4722657cdfb13ded895f089eac23da5835281fe1dec4fe041ee5d2bb5cf0dfe3a1388b3f4bb1c898cdc9e16b9bfb2880798d1f892074ef35c4524f3212f181020c012f6396f9a5f90968871334397d331a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000000000" + }, + "result": { + "gasUsed": "0x1bb5b3b", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x89d1a98260f8ce9a489e96618fc2eb9a71aae288a0dce7e576cc9f92777bf78d", + "transactionPosition": 16 + }, + { + "type": "call", + "subtraces": 4, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b2b89", + "to": "0xff000000000000000000000000000000002b620f", + "gas": "0xa24a14c", + "value": "0x1411b1db93e7400", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000089a8194870819ba01d82a5829000182e20381e80220093c553e711af9713a922b0a6a904eb17092ad8c5f5f4dc072c47235c4befd721a0037dfff811a045b26c31a004fa108d82a5828000181e2039220203f3de710bca81902e9c52aa8a856501d7597cb246059090e1a122a6bf3cc0d24870819ba91d82a5829000182e20381e80220d680339874b015ea1e6f62258dd33039d11d6e52ca2ed52d75710390146ef9031a0037e0a5811a045b33e31a004fa109d82a5828000181e20392202065c3e8e0b9d5e7a3c98c22481ccd92a02df2e940dec694d41b65b8a159bff530870819b948d82a5829000182e20381e80220c8ba717e0876f47d552a01c73caaa0a474fcb68d5191646b77faf23543bd80361a0037df37811a045b123b1a004fa10bd82a5828000181e203922020ecd65fb1dadfb1c73d465961ab5670d04ab5f16778ba538e377863628da9210f870819ba8ed82a5829000182e20381e80220d4308d1779e1d8a0cecc71a1bad51c00d5e8548f97897e393f492a98e7605c4f1a0037e0a1811a045b33e11a004fa100d82a5828000181e2039220208da956908f35641d819d6ca593f70da0f0bc50178320c0f2b5d98f845e225908870819ba3cd82a5829000182e20381e8022038edc7a8c7b4a3709dfacd2740137e8e129142ffb8e927ea5a9edbb6c64567251a0037e048811a045b2bef1a004fa102d82a5828000181e20392202067e6ff31806a59a94409982748b7b95b0b3e111831a4cd1082e45c28316a6412870819b925d82a5829000182e20381e80220b813d4997aa7758f20ff8a61a3c85a424c095600f19affa27f131388351df4501a0037df06811a045b0e301a004fa104d82a5828000181e203922020fc03bc87e425b22b4ccced611f45235b986b85f108f3f2549bcd4eee9290cd2f870819ba50d82a5829000182e20381e802202482832ae44ad6cd1a2e6a2b7619b26b7b7149eabc950010bc1866875d029e1d1a0037e05a811a045b2df11a004fa106d82a5828000181e2039220202661ae0bcacdec750b339f0ab0ce9328f2692058ae1045968e0edd5f7b3dd811870819ba39d82a5829000182e20381e80220dd0de1bdfc9c1619c66b73b4bf977f3b3447c68c27aa10ded4555a12ab3a2f511a0037e047811a045b2beb1a004fa107d82a5828000181e203922020d1d07a5a06841c47767b57ef90892dbaa6023e96124a3f76148c58368cca2617870819b9ded82a5829000182e20381e80220027e1870716e523c0f3d08715b2a1490e446125fbd7d2e8022fd303e3c56f93c1a0037dfe1811a045b24031a004fa108d82a5828000181e203922020f29aacc7c8fe59516a3b527547eaf8d250d2f48f981cbf311166fb0d9c031808870819ba6dd82a5829000182e20381e802206875104c4ef4690f4589759cca1f539fa69e9673deb29a68453abc7140da57511a0037e071811a045b30281a004fa110d82a5828000181e20392202009cbd082f5c1b3806813ae5240871a7e99adddec8696ccb63409829969e90513870819ba15d82a5829000182e20381e80220880db767d7ad153fa104053668a3dcefa92d626d76e1abc9f685674f1ffbc83f1a0037e017811a045b27f01a004fa0fed82a5828000181e203922020d102fcda4fd8039589c643cb8a40c68f6fac58b780572d5c786ec3d911c65221870819ba38d82a5829000182e20381e80220a94505e3725f268c7cb9ad1ee0538de448b2999894ddced47e43fa43dad37d4f1a0037e042811a045b2bea1a004fa105d82a5828000181e2039220200cb6f8071eab0af5717dfe15386545450b7e931cd9c08dcd47f5ef6006f40d34870819ba69d82a5829000182e20381e802202012760d042b85e82cb9f87fed5736c0936e9d4eaf003de6074cf35de125c5591a0037e072811a045b302b1a004fa109d82a5828000181e20392202000068b68d6d878c79e8d6c3747c52c39ff17f60bf6e9c8baa0db5e487a6e4105870819ba55d82a5829000182e20381e802208966b25c9e3954eca9ea62c2ef736fe1a78e72d1d9b2c1030f4a9f0af1ec5f321a0037e063811a045b2dee1a004fa10ed82a5828000181e2039220206c73256ff2c356844554e46940dc1df9fd03156c148b40a39732db2b1bb9e228870819b9ecd82a5829000182e20381e8022013f73f929cd0028ba741b3796b8d8ea1fc7bd22140e228767573ef84619666231a0037dfe9811a045b24ac1a004fa110d82a5828000181e20392202017c9587ea90cc9d2394afe6ce5e78ba20b0282aff1fecef387465f6903d73d33870819ba2ed82a5829000182e20381e802206f9dc8dc410c97bedf1b7cc1d72d15abf1e7918f84cd8dab15ef1cf9889070091a0037e038811a045b2b0c1a004fa0ffd82a5828000181e2039220203565803f11d30abdde5e2c8f99bd2160791c62175345f68c20aedc61110fb53f870819ba0dd82a5829000182e20381e802207c7241beb346264e70baef27c4191100515937ee353cf04485e718f03d537a641a0037e010811a045b274c1a004fa102d82a5828000181e203922020781476353489a83554fb6ec661e03fd81e2f92a8de6b7871ef76607d9d636127870819b617d82a5829000182e20381e80220f1841d865f1de264b0c50c7f8e653e61ed65734ac0ddaace8089d1049d9f855b1a0037db02811a045aaec81a004fa102d82a5828000181e203922020964507149377a01e67dc8ece2da6c9b49418687d75676f351c70567ff9628821870819b9d3d82a5829000182e20381e80220134d720e957f2dafa018b497bdef3d69bc67c3d429c43562b3a6211ce76102591a0037dfca811a045b22b71a004fa103d82a5828000181e2039220204ab78880ebe55d1606957e0b2e9ffcbd599b9545d5245c48f4db7b1b0e7b0b07870819ba97d82a5829000182e20381e80220a32b01b61da64c40030ef19ecc0b07aa93be21853fcfe6573a3a753a58da2a151a0037e0a7811a045b346f1a004fa10fd82a5828000181e20392202049e915acdfd6178f081d37cd17d86c8143415a65a2d6d4d72c52998888679332000000000000" + }, + "result": { + "gasUsed": "0x61131c0", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x578dcd2316d11d619e685216afa861d1eb631f38b4fdbf4a189088485f2db90d", + "transactionPosition": 17 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b620f", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0xa0a644f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x578dcd2316d11d619e685216afa861d1eb631f38b4fdbf4a189088485f2db90d", + "transactionPosition": 17 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b620f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x9f99f07", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x578dcd2316d11d619e685216afa861d1eb631f38b4fdbf4a189088485f2db90d", + "transactionPosition": 17 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b620f", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x9e66af0", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000106819483081a004fa108811a045b26c383081a004fa109811a045b33e383081a004fa10b811a045b123b83081a004fa100811a045b33e183081a004fa102811a045b2bef83081a004fa104811a045b0e3083081a004fa106811a045b2df183081a004fa107811a045b2beb83081a004fa108811a045b240383081a004fa110811a045b302883081a004fa0fe811a045b27f083081a004fa105811a045b2bea83081a004fa109811a045b302b83081a004fa10e811a045b2dee83081a004fa110811a045b24ac83081a004fa0ff811a045b2b0c83081a004fa102811a045b274c83081a004fa102811a045aaec883081a004fa103811a045b22b783081a004fa10f811a045b346f0000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2183932", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003728194d82a5828000181e2039220203f3de710bca81902e9c52aa8a856501d7597cb246059090e1a122a6bf3cc0d24d82a5828000181e20392202065c3e8e0b9d5e7a3c98c22481ccd92a02df2e940dec694d41b65b8a159bff530d82a5828000181e203922020ecd65fb1dadfb1c73d465961ab5670d04ab5f16778ba538e377863628da9210fd82a5828000181e2039220208da956908f35641d819d6ca593f70da0f0bc50178320c0f2b5d98f845e225908d82a5828000181e20392202067e6ff31806a59a94409982748b7b95b0b3e111831a4cd1082e45c28316a6412d82a5828000181e203922020fc03bc87e425b22b4ccced611f45235b986b85f108f3f2549bcd4eee9290cd2fd82a5828000181e2039220202661ae0bcacdec750b339f0ab0ce9328f2692058ae1045968e0edd5f7b3dd811d82a5828000181e203922020d1d07a5a06841c47767b57ef90892dbaa6023e96124a3f76148c58368cca2617d82a5828000181e203922020f29aacc7c8fe59516a3b527547eaf8d250d2f48f981cbf311166fb0d9c031808d82a5828000181e20392202009cbd082f5c1b3806813ae5240871a7e99adddec8696ccb63409829969e90513d82a5828000181e203922020d102fcda4fd8039589c643cb8a40c68f6fac58b780572d5c786ec3d911c65221d82a5828000181e2039220200cb6f8071eab0af5717dfe15386545450b7e931cd9c08dcd47f5ef6006f40d34d82a5828000181e20392202000068b68d6d878c79e8d6c3747c52c39ff17f60bf6e9c8baa0db5e487a6e4105d82a5828000181e2039220206c73256ff2c356844554e46940dc1df9fd03156c148b40a39732db2b1bb9e228d82a5828000181e20392202017c9587ea90cc9d2394afe6ce5e78ba20b0282aff1fecef387465f6903d73d33d82a5828000181e2039220203565803f11d30abdde5e2c8f99bd2160791c62175345f68c20aedc61110fb53fd82a5828000181e203922020781476353489a83554fb6ec661e03fd81e2f92a8de6b7871ef76607d9d636127d82a5828000181e203922020964507149377a01e67dc8ece2da6c9b49418687d75676f351c70567ff9628821d82a5828000181e2039220204ab78880ebe55d1606957e0b2e9ffcbd599b9545d5245c48f4db7b1b0e7b0b07d82a5828000181e20392202049e915acdfd6178f081d37cd17d86c8143415a65a2d6d4d72c529988886793320000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x578dcd2316d11d619e685216afa861d1eb631f38b4fdbf4a189088485f2db90d", + "transactionPosition": 17 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 3 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b620f", + "to": "0xff00000000000000000000000000000000000063", + "gas": "0x2176414", + "value": "0x123ea1b057e9800", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1770", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x578dcd2316d11d619e685216afa861d1eb631f38b4fdbf4a189088485f2db90d", + "transactionPosition": 17 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d33a0", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x548da5a", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009185583103b39880989620c017d43ab24caf829d6d2b1cb27401aa18ba501d87d32526973a498c8879fb1833f2c4198c3634753bfd583103b39880989620c017d43ab24caf829d6d2b1cb27401aa18ba501d87d32526973a498c8879fb1833f2c4198c3634753bfd0d5826002408011220b6da1051bedb96e0c5636bad2656b365291dbd94f1482642b8a5e51edffaafad80000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x170f6f2", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001d824500e689b501550278e8a50631934966637b6ce6ad7ca7e3c68c5995000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x86ccda9dc76bd37c7201a6da1e10260bf984590efc6b221635c8dd33cc520067", + "transactionPosition": 18 + }, + { + "type": "create", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "from": "0xff00000000000000000000000000000000000004", + "gas": "0x53cf101", + "value": "0x0", + "init": "0xfe" + }, + "result": { + "address": "0xff000000000000000000000000000000002d44e6", + "gasUsed": "0x1be32fc", + "code": "0xfe" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x86ccda9dc76bd37c7201a6da1e10260bf984590efc6b221635c8dd33cc520067", + "transactionPosition": 18 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000000fce75", + "to": "0xff0000000000000000000000000000000001b5d7", + "gas": "0x2985b7c", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000279850a8382004082014082024081820d5902408fc9978c8c1861e78438a099a06de4d9b807567e3eb578fde1ddbd7c88c23ced96d89887d40e41888f6a359ba0d98f3e89d8003c62be48cb4f7a6d8132329e1e18480a9e89a39da05dbe12d48d027bdeac20d8d46c2e9889ab76a7800cffc27212d72abb73090cc21af180b27add3ee619abcc012a98145bbd466dfeaad245901a7f1106a77ee7bf9818680a028229cbb9c6c1c32b329a01fd9ca68308c92399fac66c67707d3efd760e9b5a0d6a6fad89a425c6ec2bc654e1693c3679059c9cb9543366f77db1c65728b88a98d317b17dc66e3bfe2d30c85cd1e883be09ef4fc6ab3b50a684042e30627cd01b67d964932e9ec0d8608ab66b7da8dc9fd99480e517bc58aa752fd9bddcfaa4aba5f59101d108684a40a8ff3d954d5a58c230fa03ab135a17fe691663d1eceeff68d541a69eb913f0fe3b266dcf66543ee36e374c6596525ade50dd4845347e445773a29476a841d05c79df4ddb1904aaae2473cc9bcba41de75f0072bfe0b7df94aa3231044d2d1f8ca32bb7d4707920918ad9b31854e6791623c69c5cf6bbd57a7589ad39c8cb72459b24099d210fa1b4e11351185315739e2853a81ddb02286907bd9804026f2b59ea68462a4e56263960a2a8f4c20f625f87e13d47cedb499fdf4cc034f010fc170b505540200603103d360231fe050e5d769ce47f97ecf0ee862a6225f91fb57b2c5fdddf11e96f69f68742371714ef808477c8ca21f339ca1273b0aea3c066ad68f49784cc6ec88a7dd3af0408e61e6dcffbd19b9811107d960c137c7225b0c783ccb69194d1915085781a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000" + }, + "result": { + "gasUsed": "0x226bdfd", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdfe4b81a94cce7b6ca32cc497e32a8447786ceac845e366a9b2fcbcf16971a6d", + "transactionPosition": 19 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2afd", + "to": "0xff000000000000000000000000000000002c2c61", + "gas": "0x2ca9cf3", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000072818187081a000114cbd82a5829000182e20381e802202a0b230182f9610882b85622ee618ffa5933e6e6fc3e70253837708f7fae630a1a0037df72811a045b19341a004f65a4d82a5828000181e203922020fa592a0514dce829ad6e2bbe0fc789d094183e20304be069b5b9393182ffa6030000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1df238c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x76fae58091c030e252484105473c8950ef633dd02b4ac8892b341bf46c77d528", + "transactionPosition": 20 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2c61", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x2bf36a3", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x76fae58091c030e252484105473c8950ef633dd02b4ac8892b341bf46c77d528", + "transactionPosition": 20 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2c61", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x2ae715b", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x76fae58091c030e252484105473c8950ef633dd02b4ac8892b341bf46c77d528", + "transactionPosition": 20 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2c61", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x29c5bea", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f65a4811a045b19340000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x477778", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020fa592a0514dce829ad6e2bbe0fc789d094183e20304be069b5b9393182ffa603000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x76fae58091c030e252484105473c8950ef633dd02b4ac8892b341bf46c77d528", + "transactionPosition": 20 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2afd", + "to": "0xff000000000000000000000000000000002c2c61", + "gas": "0x2e2d67e", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000072818187081a0001160fd82a5829000182e20381e80220ed53117c4c68300789b739922ced240b43091dcddfed253062cf17677f9775141a0037e0e5811a045b37331a004f66fcd82a5828000181e203922020c775d47a111888a1ad17dd5989a957c59e7a2e89cb7df8639a15556593cb382f0000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1f29209", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbc62a61e0be0e8f6ae09e21ad10f6d79c9a8b8ebc46f8ce076dc0dbe1d6ed4a9", + "transactionPosition": 21 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2c61", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x2d7702e", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbc62a61e0be0e8f6ae09e21ad10f6d79c9a8b8ebc46f8ce076dc0dbe1d6ed4a9", + "transactionPosition": 21 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2c61", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x2c6aae6", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbc62a61e0be0e8f6ae09e21ad10f6d79c9a8b8ebc46f8ce076dc0dbe1d6ed4a9", + "transactionPosition": 21 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2c61", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2b49575", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f66fc811a045b37330000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x4769b4", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020c775d47a111888a1ad17dd5989a957c59e7a2e89cb7df8639a15556593cb382f000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbc62a61e0be0e8f6ae09e21ad10f6d79c9a8b8ebc46f8ce076dc0dbe1d6ed4a9", + "transactionPosition": 21 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001ee034", + "to": "0xff000000000000000000000000000000001ee031", + "gas": "0x1b3a043", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f385181e8182004081820d58c0a7a02e8dc005044807e4888bd0823f61ae169669e7a27a3b7e3912b8fba0eea593c30732f4480a1fa0e285867e448306b2a2ddea74ce0511ace188bd92cee9e2352a2f1b70a1903b268fd124d1e8e6f92eefbd3bc7a0fc42a477ace39972b9290963cba107a2a981b7c74c1a3dcb6be6a2b7a01e8224501b0a4371f1a3970c0664b313bfcf7be778165db0e2e4bf634da61561da28de009a0cba108ff65d334ac272e4b3ab6e8e26fc23cc09439290bef48866abadb58407af3dced632acf3a71a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000000000" + }, + "result": { + "gasUsed": "0x1696c22", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xcfeff915c7a3e06a7aafbb9f1a20ef023615aecea6cb6b004dc87ead55f63702", + "transactionPosition": 22 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002af885", + "to": "0xff000000000000000000000000000000002afa9a", + "gas": "0x466a958", + "value": "0x1a7b47481750efae", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a0001928c5907808a218c1f6baaa41a2cdb879b1b7bf7cc7a382348970e0520fea435085566ff96b13118cf3c9e35bcac55bee865ff570fa7ec2b266e385a473c7e0e77b1080bb1392cd15fa24f39afd648cbf45b2b99d23aa58e9da9e75476fd4617cee6b3bd4708bf6b5afc9237e1977040f8e0bc63c5d825fbb6c3e471f073a6a96f27f9c30b98658dc8e49f6b7aa65d02aef573b613b579c462df8e45632b979c1baff7445e5645163020e17829f0aa7effd318343387715eb7a7eeed87e366620522a4dbac8e916703773123ad4a1347f8e59285eaeb6db1d236a503cbcb2c19a9ac9c6d2cb110adc7685a38df6873db056779f3a7b06cbac8650260481fdc488b9e2b75a7a3676f237768f3d45dc824ced53e6e34b8b004a457f28eef44cadb4334afea05145b1ea8103363bf1b88db266f14971d72b024f9b7f7b8ed9fa4100c388eabd162af274f019d2ef4bd90d53a903ebd5ca39e0478f6173bbd8a576648f2291f9eede03ce9c41aae4d56489652d0a74e7275f599a8aababb97676b5538ad1960cc8260a4e8a6b218ecbd54c8b5b91457ae56d3cf199e740499ddcd1eca823550817a43c1e53d4e407a7e7443f4f038e337ab1e959c101179efd603f2f9b7e40e6255be393e233ccdf601f2e423e898c286776a0712c4db8ed22e8619923849828d10f7cdddbce7e3bb2d51ecd9c3401ac3762fc44f4848f25fc2e8f22059e43ce8b9d39e628ddee9330ba22cb55da67aab985e6022e877568011d3e3eee84df1cad6cc924a426ae8c338140777f6e592c7872a85357931e586ad3f3684c884994495d20c30c5ec5f6de0244355de4b1fde1117b1068f64372ecdcda183e8215032a2d18157d59eb1aa0e09bb1e85fbcc0ab978e1860f6f457cb821a266bdfd197c53516264feb015c8562144cb4fa2c8a6918c6d5e08d3c050136f2905d373c219149dd6c37662da580148312067cbc440a9adb3ecd5df0cb01eb6ad1d4fcd7399b3fe43bcb7397abb7fe9323a71a2a290ab2b0e955979de28827c98e38fbc7c65d63a22e4f6dcd5077e182801e7821d59408cfb01bf70be7a5d063a48b51fa3c6899da9e902cb9305a68f74f4e32ecc6186130cec8daf78f4d40ec869286130ed41228373e496471376ab5646ee44b6f8ab4c4c9210557b7ec166fb4582fc8bdf2fe12a80e37cba8cabf8c7456b75a305aa0aabb6dc1e040ca088a28672bcad1a17c4b661611cae0b5d968c04df8388a06e69fb302cde54f5a1619c0275b1d7349a41561ce6476a8ba3bf3ab1b4f291dfa72cff5696f4203c733f78f8168ffbb2c97392db60ab160a6f42f1c3b8957e24104283deb670393aa20c5cac99bb54489620f16e0838fdb5b7f92515929745b58c2769d26374cca5d94cfb14e133fd3df40497a1fa9ac70a9e9a295ff659b04aaf81a5436dabde72e10af28a00a8f05cd6b3e6bc75847ca7515fd27d4b2cee97e21594d4a5b659a0504e77ccfcbeb082043f73dbc1484e5504b09c048c8a3a6c2db1278ac2205c78a48263e9ada4646dec7d27f28392667d3fb49dc101a5d1b6aade897bb92c692b956b9a83d7d2abfbdac1707a9d369cfc5260ce380f67c3ae6337e34c272ea646632f2c73465b1a13b358cdad9d4583a59cc3be1c8f2094282560aa83435074686f6abec5b4c895452ae74f92d9bbf6e9ec9728d07c397aef8611d09e75c00dfc297af2d724877127d5abca2f6a530ebc768617ad077d4712b26fba3cab83c6c74a7119a6623cb9a712eddadbd7811fd3948337dbdaa311f9891f96a324c20b56ef6d73450c5d3804dac0e51fc07fc82eddf441c9412e13efb701115362bf844ceb88793a131e136a0ca9935b7025a6682afbb8685c9b058c833dcd81e93e6dac958a31f5b942c4578ddb058ae644dddcb43fd9f785510c7a2bde7532b3f84b48e565cfb0ec1f55284ef355df662f091a401a52122823b46697eb141523b39dd9eeb73e10df2793a4e2107ee299405800df9e95340723b18e0d6c46fd7c4236ceaf34594a4c652b22145361f277fc5b3427d47e5b6c51db78e5b00050bc616c2e6efb54eced079a1a7186c080ccc3bab5989e0df45115d314a95664fff63ec61f73ec34c22033105410d1158e44456ba982984508a8c650d912c1d3fab14f7a12935ea1142eccb0748ddd33f1bcc07cd5614e24257ad9a656cfc4a8589042de2b29aa641b0e59b8fb2d83a0600a72c43d44b0da9f7df90985b27787686d14dfdfba29c895dce4ea9b827f5dea7d12d0536e36a90f98cd8e9078ab343ddc20a35dedf9e46010513eca0141bde7b2ab975abe920e201e7c55375eb7c1fe56b6677eed219a59d85c72e04ca348fae0171c2b4b33dd80e743954b80945cbc97c225e28be27d67c7425a9d0331f74919002b5cc2a120f51725bddf5bd8661f5a6c5909561f579536577473a6c8fadc7ca5b0a415d49e78441c3cc0d3b4e26d089bb515fb1a47d7a54814349b6e1cfebd137c2cfe94d3bd2ddcf7ae976a61f32638e55e0e290b6fdf379fda9428124380187eea5fc2a7ef55c7135b432d95b4916e594c3331dd28419a263202c48deb804ceb2119646cd56f1a907a9b4e723d582d1762c0c667b4ad1fe7887531540f19018896f660d57efbeece42880a36772b04b2b438adfa4b3f8c5c2474cad4d99d2e55a165ae536fd7713f3564464fcc1e193c170a0965bc4f0d1bfa0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x70ac08", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa714c6be95aa4ef44699b9098e264c0adead1caeffac75013c413a7a9798dcad", + "transactionPosition": 23 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002afa9a", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4271824", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a002afa9a1a0001928c811a045b25725820ebf616e5e6a85ffef1fe235d974420b8a1c0a70d4fa60d21d2215f7e2aac05ff58207bb5ec49d8afc9b6444096fb05e348b164acb4a9305a4fa411def29c62edc61f5907808a218c1f6baaa41a2cdb879b1b7bf7cc7a382348970e0520fea435085566ff96b13118cf3c9e35bcac55bee865ff570fa7ec2b266e385a473c7e0e77b1080bb1392cd15fa24f39afd648cbf45b2b99d23aa58e9da9e75476fd4617cee6b3bd4708bf6b5afc9237e1977040f8e0bc63c5d825fbb6c3e471f073a6a96f27f9c30b98658dc8e49f6b7aa65d02aef573b613b579c462df8e45632b979c1baff7445e5645163020e17829f0aa7effd318343387715eb7a7eeed87e366620522a4dbac8e916703773123ad4a1347f8e59285eaeb6db1d236a503cbcb2c19a9ac9c6d2cb110adc7685a38df6873db056779f3a7b06cbac8650260481fdc488b9e2b75a7a3676f237768f3d45dc824ced53e6e34b8b004a457f28eef44cadb4334afea05145b1ea8103363bf1b88db266f14971d72b024f9b7f7b8ed9fa4100c388eabd162af274f019d2ef4bd90d53a903ebd5ca39e0478f6173bbd8a576648f2291f9eede03ce9c41aae4d56489652d0a74e7275f599a8aababb97676b5538ad1960cc8260a4e8a6b218ecbd54c8b5b91457ae56d3cf199e740499ddcd1eca823550817a43c1e53d4e407a7e7443f4f038e337ab1e959c101179efd603f2f9b7e40e6255be393e233ccdf601f2e423e898c286776a0712c4db8ed22e8619923849828d10f7cdddbce7e3bb2d51ecd9c3401ac3762fc44f4848f25fc2e8f22059e43ce8b9d39e628ddee9330ba22cb55da67aab985e6022e877568011d3e3eee84df1cad6cc924a426ae8c338140777f6e592c7872a85357931e586ad3f3684c884994495d20c30c5ec5f6de0244355de4b1fde1117b1068f64372ecdcda183e8215032a2d18157d59eb1aa0e09bb1e85fbcc0ab978e1860f6f457cb821a266bdfd197c53516264feb015c8562144cb4fa2c8a6918c6d5e08d3c050136f2905d373c219149dd6c37662da580148312067cbc440a9adb3ecd5df0cb01eb6ad1d4fcd7399b3fe43bcb7397abb7fe9323a71a2a290ab2b0e955979de28827c98e38fbc7c65d63a22e4f6dcd5077e182801e7821d59408cfb01bf70be7a5d063a48b51fa3c6899da9e902cb9305a68f74f4e32ecc6186130cec8daf78f4d40ec869286130ed41228373e496471376ab5646ee44b6f8ab4c4c9210557b7ec166fb4582fc8bdf2fe12a80e37cba8cabf8c7456b75a305aa0aabb6dc1e040ca088a28672bcad1a17c4b661611cae0b5d968c04df8388a06e69fb302cde54f5a1619c0275b1d7349a41561ce6476a8ba3bf3ab1b4f291dfa72cff5696f4203c733f78f8168ffbb2c97392db60ab160a6f42f1c3b8957e24104283deb670393aa20c5cac99bb54489620f16e0838fdb5b7f92515929745b58c2769d26374cca5d94cfb14e133fd3df40497a1fa9ac70a9e9a295ff659b04aaf81a5436dabde72e10af28a00a8f05cd6b3e6bc75847ca7515fd27d4b2cee97e21594d4a5b659a0504e77ccfcbeb082043f73dbc1484e5504b09c048c8a3a6c2db1278ac2205c78a48263e9ada4646dec7d27f28392667d3fb49dc101a5d1b6aade897bb92c692b956b9a83d7d2abfbdac1707a9d369cfc5260ce380f67c3ae6337e34c272ea646632f2c73465b1a13b358cdad9d4583a59cc3be1c8f2094282560aa83435074686f6abec5b4c895452ae74f92d9bbf6e9ec9728d07c397aef8611d09e75c00dfc297af2d724877127d5abca2f6a530ebc768617ad077d4712b26fba3cab83c6c74a7119a6623cb9a712eddadbd7811fd3948337dbdaa311f9891f96a324c20b56ef6d73450c5d3804dac0e51fc07fc82eddf441c9412e13efb701115362bf844ceb88793a131e136a0ca9935b7025a6682afbb8685c9b058c833dcd81e93e6dac958a31f5b942c4578ddb058ae644dddcb43fd9f785510c7a2bde7532b3f84b48e565cfb0ec1f55284ef355df662f091a401a52122823b46697eb141523b39dd9eeb73e10df2793a4e2107ee299405800df9e95340723b18e0d6c46fd7c4236ceaf34594a4c652b22145361f277fc5b3427d47e5b6c51db78e5b00050bc616c2e6efb54eced079a1a7186c080ccc3bab5989e0df45115d314a95664fff63ec61f73ec34c22033105410d1158e44456ba982984508a8c650d912c1d3fab14f7a12935ea1142eccb0748ddd33f1bcc07cd5614e24257ad9a656cfc4a8589042de2b29aa641b0e59b8fb2d83a0600a72c43d44b0da9f7df90985b27787686d14dfdfba29c895dce4ea9b827f5dea7d12d0536e36a90f98cd8e9078ab343ddc20a35dedf9e46010513eca0141bde7b2ab975abe920e201e7c55375eb7c1fe56b6677eed219a59d85c72e04ca348fae0171c2b4b33dd80e743954b80945cbc97c225e28be27d67c7425a9d0331f74919002b5cc2a120f51725bddf5bd8661f5a6c5909561f579536577473a6c8fadc7ca5b0a415d49e78441c3cc0d3b4e26d089bb515fb1a47d7a54814349b6e1cfebd137c2cfe94d3bd2ddcf7ae976a61f32638e55e0e290b6fdf379fda9428124380187eea5fc2a7ef55c7135b432d95b4916e594c3331dd28419a263202c48deb804ceb2119646cd56f1a907a9b4e723d582d1762c0c667b4ad1fe7887531540f19018896f660d57efbeece42880a36772b04b2b438adfa4b3f8c5c2474cad4d99d2e55a165ae536fd7713f3564464fcc1e193c170a0965bc4f0d1bfad82a5829000182e20381e80220301293ec00df8e1cf2d9bf338279beaaa9a9a471701f60f290da4576068acf01d82a5828000181e203922020d56877a200472eb74a6521299547efbfe80cce790d635b75516278eb1731941900000000000000000000000000" + }, + "result": { + "gasUsed": "0x2ea25d1", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa714c6be95aa4ef44699b9098e264c0adead1caeffac75013c413a7a9798dcad", + "transactionPosition": 23 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c17d8", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x1c5e5bc", + "value": "0x2db18102f13817", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000054400ebaf70000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x160c586", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x52aaaa21fb1cee8882aef391c310f8a26e6dc9a5d3cb968f2f384477dd6851a9", + "transactionPosition": 24 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff000000000000000000000000000000001c17eb", + "gas": "0x1b32788", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x13a470", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000128345008c8ab4014400d8af70814400e3af700000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x52aaaa21fb1cee8882aef391c310f8a26e6dc9a5d3cb968f2f384477dd6851a9", + "transactionPosition": 24 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001f3b4d", + "to": "0xff000000000000000000000000000000001ee777", + "gas": "0x199d203", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285108182004081820d58c0962920987d861c8b39327edda8e2adeb06abdb019d3aee70b32a296b43255088b551b860b2047e2362f6a9a8912ab69ca9ba5d09d7bcbfa7d5876af95f05fae5c17c6addc4914812aeb2480cd0fc42494879621ee9917e51828638cbb37f8ac50fb1113b1867527112595aa44ec24261ace85a81af4ca83ad5f3f2cbf1968c7b0172351332ff5b9163b9347f898f329294387bcbb1c4f82a2ac478d1db646a0be71d297d2fe1969e37bc5540e90cd7978d45bd21f3e042c45afd6ee8eb2dab1a1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x154c312", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8bfc485414cfc17776059a5ea837757f4df29dde3956b025bffb9dcc9706b3ff", + "transactionPosition": 25 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce37d", + "to": "0xff000000000000000000000000000000002ce3ba", + "gas": "0x41f9a33", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000007878219656359078089b3567e087d9f038414ae554aea4607bc0558961eca169c7a8d57e5bb6408861bedd976ad9c30ac5cba0d0da3af1de68cef5b46465cc730f3ced2fbdb031d392b950ce381b7f12967fc57998aa6f668cc8c5b553fb27ce9dc831e13666d13c007a2516ebe2ab906bd22cbab92d58f874b6f98a4680aa6d8790de18a3090df8b0ae3c90adf836c7b7b3f39830623867ea9c0e007f23be47549243aa4ce36277271a70e527ed4241d8496a27925d45e6898914cf1fbd2d118751b5b6cd292adc18891ab78a9153cacda89d3019f1ca81569cc4570725c70fde9d293bea660ce216b720bf0d27bba7121f33bdb9fb450e385b2d68d890711e47e0e7c98f0e38e0a4cbbaf8137be683ec840c651f4051b00c3ed7e825a6a05fcd81b98502c8c0d280b33fef4c8fe2c0d8dc8bb3d31f96558004f6a437dcd9c4c784ec5f19f9718480de4cbd8c354687bf9c80f0056eca127a37a5a80cd2f268ad24e5e7506b627ef5989354ac09beb5e95e9e202b4c0675b6ea01843afd38ce840a0a97d3083771e956a1040fd7bb21ff1040cd3d025beb0e32ce10166acf3365941e7606b9d021df6ad162f58daa88591e69d94ae13330dafe128e8fc8098ec8011d88e40058cda8de05fc93cbf3b1e81abd599f3ba177f0908627e954fb703a027c72c0bcd4fb9085f59cd9bf2237fb7476e866cc5b9d52fa6269f912f49f7bec55000e8e9166c10f6b0a587b9075cbbb06e9129ee4cf2ad0d222bcff3d3db550ea803bd3d993feb24128f4f27081caf42c65f39bdba76215f32815ed5db1df935bad67101314cade9087e8db56416dda958e20cb1b3ddd5df3739d66d06564f1acf725706aedd6c2a9ec15ec1371690e9581a75f29ff5a09a629e45ae13e6d5d94a5bba0242a2bb4ddabda70b62a3717a942dab67d4112d8b7325aa5cf54233f8c686e9d6b275123dfd791f93db568f8cccb0bea7cf8deaee33696314f092b04b6c8ae4b26e64d3fa436bb49d87205719324c2196db84b758440ee9ebf8b38f60799c26ff8e6237d92f183d94e3213cbe5285b8a4deeb80bc450899c97a0c3383ad17bcdf337599a7461bc3cd7a7b82116d715109cf7564dedc7ff071d502452267776e485740886090a92ebbf802e37ac635966fd8ce8157b93a0fb442f73207996136049fdcd81be00bbb015918683d16427df44576b27ebef8471bbc5fd1bac62006314c1117086f3352c36a7c68abe2054f70d999155445a87431d0cc42df77a1a1d909b92e78cebe4260e86821b1015ab8072b72b255072a7b87b1afbb78f2604c783d84e4a586f248d26720f5402046a48aec8b8999fb958fcd5c1ce873a3b96c95c9508e349d0df5608404fb0a0a9e459c746eb9fb655f4a6d719d3117042c76e202cc3b1d530c8a6f313f2d160d193596244aacf5e704055004579c2a83c5963d8ab595604d10bf98e9d80681c698bf8b9bcde33df3bf3169ab754f5f1fd862adf9250aa422e1611ebac1eba3cc793662894288a724fbd791e89dcdd7a3bc74ff9aee5294be60e991748a2fa432205cf2226cb232967edb4999a3bf0643be318de1296f684b86cceb1dd56b2cdc486c8ec1a6d788568b835318fb355fdac8f9a34cd68725d7cd120372a7f636002fb17f1d10ddb0e674a7e86b98391a77d862325b5195109ebfdb3d8a1e27a7f79d0f7c567cad351826467a63ef149cf5dbfab8399a4012da12af9d4ff6598b0ae54c05775dcf85c0191d38e339e222ead9376c8ee915df379c5b1b42abe15048667290fe02ce9783cc46f10d35bd600d530292992b08ef959940dabc733c9e60fec0ccb388a427a3bf07ded414b478c91d2026df0a52cf2c25aeea9849c86480cfff3600c43174f5d4451df4962a4d9cebb9e2f11797679d4d28533baafa05c253c25a4f62ab3d0190278e8c20fd72662a023e60e0e3eab027090e07716dce1251cf730690b2bf033236d3fe537e153201455032f691e2516a02c1e0a999ceb02c5709dd672c70897838f57a4f6a854ae13c157d5b006677ca44af39133b0663ee2e2beb1fe4a68a846ce3cc2114bf80f91e187da1f0b250fddfdfde59eee0b656da275deb9490dbc7289643c91c94500771e373fdc338251f35567d0bf69a1281300e5f8f5f2df9c3ae790e5d8493c9176ae7048496383654e519118d249be0e47252aeeeec73ce3954722f4ecbfd6dd0a931e35970085692b3c1592022fa557733d6125793516ca85a7b27816862c7db7ecf74a5c01a50caa3997f023a836c410178c402c610b48b97854c8c3b2522a3c73a176c08f86a3d9bcd31c513d54107ed0e87ecbe6a8eef753d7ef748b49ce8e07f0240214d85463e7cb2cede2738ae1ae0ae628abe17f62b3e75cafa9b5065e5469a13bd04e1c7f96924d7fa6464191b9ead0479821d0114ba27f1cdbc1cb935e1fed086c8a53dc38245a373bc68cdd8f01a84d9c5fe093ff831c7ab0606d1179e52877caae79c9112cc5af723d1d6bfaa0515b8313eaa7c65daef498853daccf0345999610f6aa633d04805b14c1a5a34b9c3a75f758f3004e62464fe3f23255148ee02029002108728d91a4fb0f8af2a2c3a44f3d97c810e03a926116f125a42c9d8da8c93da016e97f3e1b50a2fb838b23eab8b279f95fcc21cedbf474b3457ac3a3d791f5d188b694285ec2d1169ef8015203e11a8f28ab520c57b37b9390cecb600000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x695660", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc181546e031e0c986a730e2567075d746fa9a148c12c16b15c36e689dba86a87", + "transactionPosition": 26 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3ba", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3e72861", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002ce3ba196563811a045041cf5820cf92e6b2348466d49f180699709bf0afcc372ea7d8543074c0b5c724a7ecb6a7582039251fcab982839627fe199c46354a2e52a24e2f438c66c568c62466f8aca8ab59078089b3567e087d9f038414ae554aea4607bc0558961eca169c7a8d57e5bb6408861bedd976ad9c30ac5cba0d0da3af1de68cef5b46465cc730f3ced2fbdb031d392b950ce381b7f12967fc57998aa6f668cc8c5b553fb27ce9dc831e13666d13c007a2516ebe2ab906bd22cbab92d58f874b6f98a4680aa6d8790de18a3090df8b0ae3c90adf836c7b7b3f39830623867ea9c0e007f23be47549243aa4ce36277271a70e527ed4241d8496a27925d45e6898914cf1fbd2d118751b5b6cd292adc18891ab78a9153cacda89d3019f1ca81569cc4570725c70fde9d293bea660ce216b720bf0d27bba7121f33bdb9fb450e385b2d68d890711e47e0e7c98f0e38e0a4cbbaf8137be683ec840c651f4051b00c3ed7e825a6a05fcd81b98502c8c0d280b33fef4c8fe2c0d8dc8bb3d31f96558004f6a437dcd9c4c784ec5f19f9718480de4cbd8c354687bf9c80f0056eca127a37a5a80cd2f268ad24e5e7506b627ef5989354ac09beb5e95e9e202b4c0675b6ea01843afd38ce840a0a97d3083771e956a1040fd7bb21ff1040cd3d025beb0e32ce10166acf3365941e7606b9d021df6ad162f58daa88591e69d94ae13330dafe128e8fc8098ec8011d88e40058cda8de05fc93cbf3b1e81abd599f3ba177f0908627e954fb703a027c72c0bcd4fb9085f59cd9bf2237fb7476e866cc5b9d52fa6269f912f49f7bec55000e8e9166c10f6b0a587b9075cbbb06e9129ee4cf2ad0d222bcff3d3db550ea803bd3d993feb24128f4f27081caf42c65f39bdba76215f32815ed5db1df935bad67101314cade9087e8db56416dda958e20cb1b3ddd5df3739d66d06564f1acf725706aedd6c2a9ec15ec1371690e9581a75f29ff5a09a629e45ae13e6d5d94a5bba0242a2bb4ddabda70b62a3717a942dab67d4112d8b7325aa5cf54233f8c686e9d6b275123dfd791f93db568f8cccb0bea7cf8deaee33696314f092b04b6c8ae4b26e64d3fa436bb49d87205719324c2196db84b758440ee9ebf8b38f60799c26ff8e6237d92f183d94e3213cbe5285b8a4deeb80bc450899c97a0c3383ad17bcdf337599a7461bc3cd7a7b82116d715109cf7564dedc7ff071d502452267776e485740886090a92ebbf802e37ac635966fd8ce8157b93a0fb442f73207996136049fdcd81be00bbb015918683d16427df44576b27ebef8471bbc5fd1bac62006314c1117086f3352c36a7c68abe2054f70d999155445a87431d0cc42df77a1a1d909b92e78cebe4260e86821b1015ab8072b72b255072a7b87b1afbb78f2604c783d84e4a586f248d26720f5402046a48aec8b8999fb958fcd5c1ce873a3b96c95c9508e349d0df5608404fb0a0a9e459c746eb9fb655f4a6d719d3117042c76e202cc3b1d530c8a6f313f2d160d193596244aacf5e704055004579c2a83c5963d8ab595604d10bf98e9d80681c698bf8b9bcde33df3bf3169ab754f5f1fd862adf9250aa422e1611ebac1eba3cc793662894288a724fbd791e89dcdd7a3bc74ff9aee5294be60e991748a2fa432205cf2226cb232967edb4999a3bf0643be318de1296f684b86cceb1dd56b2cdc486c8ec1a6d788568b835318fb355fdac8f9a34cd68725d7cd120372a7f636002fb17f1d10ddb0e674a7e86b98391a77d862325b5195109ebfdb3d8a1e27a7f79d0f7c567cad351826467a63ef149cf5dbfab8399a4012da12af9d4ff6598b0ae54c05775dcf85c0191d38e339e222ead9376c8ee915df379c5b1b42abe15048667290fe02ce9783cc46f10d35bd600d530292992b08ef959940dabc733c9e60fec0ccb388a427a3bf07ded414b478c91d2026df0a52cf2c25aeea9849c86480cfff3600c43174f5d4451df4962a4d9cebb9e2f11797679d4d28533baafa05c253c25a4f62ab3d0190278e8c20fd72662a023e60e0e3eab027090e07716dce1251cf730690b2bf033236d3fe537e153201455032f691e2516a02c1e0a999ceb02c5709dd672c70897838f57a4f6a854ae13c157d5b006677ca44af39133b0663ee2e2beb1fe4a68a846ce3cc2114bf80f91e187da1f0b250fddfdfde59eee0b656da275deb9490dbc7289643c91c94500771e373fdc338251f35567d0bf69a1281300e5f8f5f2df9c3ae790e5d8493c9176ae7048496383654e519118d249be0e47252aeeeec73ce3954722f4ecbfd6dd0a931e35970085692b3c1592022fa557733d6125793516ca85a7b27816862c7db7ecf74a5c01a50caa3997f023a836c410178c402c610b48b97854c8c3b2522a3c73a176c08f86a3d9bcd31c513d54107ed0e87ecbe6a8eef753d7ef748b49ce8e07f0240214d85463e7cb2cede2738ae1ae0ae628abe17f62b3e75cafa9b5065e5469a13bd04e1c7f96924d7fa6464191b9ead0479821d0114ba27f1cdbc1cb935e1fed086c8a53dc38245a373bc68cdd8f01a84d9c5fe093ff831c7ab0606d1179e52877caae79c9112cc5af723d1d6bfaa0515b8313eaa7c65daef498853daccf0345999610f6aa633d04805b14c1a5a34b9c3a75f758f3004e62464fe3f23255148ee02029002108728d91a4fb0f8af2a2c3a44f3d97c810e03a926116f125a42c9d8da8c93da016e97f3e1b50a2fb838b23eab8b279f95fcc21cedbf474b3457ac3a3d791f5d188b694285ec2d1169ef8015203e11a8f28ab520c57b37b9390cecb6d82a5829000182e20381e80220da354058660436f1563af7b6a0642c182b1a6fa48602b441b5b47c19c1b0c60fd82a5828000181e203922020185214c7c8cb8fa5f08e57348acba88012ee10f72b1fa2e048d495d152598b08000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2ebb8b6", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc181546e031e0c986a730e2567075d746fa9a148c12c16b15c36e689dba86a87", + "transactionPosition": 26 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce37d", + "to": "0xff000000000000000000000000000000002ce3ba", + "gas": "0x4af0e25", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000007878219656f590780970b4083f06e6772b24fde9600f285c9f542ffb9b93d15ac32a9cfaa002ed367b4d8ee029ac689e0b2a9f797a52bbdd1b01687a9ceb8c9790e00f14c1a9463b0f2caa20f079f8b4aaedd577aa0e21dd971bd3bc56a3eae10200b2e1c2270170508c50c10a32fccc9206146f1d57109fc723b6aef3a814c10ff21d9f9f57e30fb5c88eddaf18dc525847f6ac1519e200e8bfa5521554b9e6519e998b5c41b099e076efdef54142293a4164be42f89a6c006e9ca0a718ebf402aadb270d9b48bbaa30f8cc5d9558d81f61871b4fd006879593d13363fd3ef2d0a8589603037a53d315f2cadbba24eeceee4e6a5b43df62493eec2ba6c2da36e7ef21140e9d7b9f2623466cf2af2d3dac9370b4e7df3439beac4b58b9021ad7c3e8aa5fcd8d76eb21157ee86adaa6ca9fafaae6a986de700eb4446e710eb8a07d95b29f39cff7c35fa5d39f985203e26e0a739506473a53390d4a05c2a890168ae9dde4a0cfc34e7a215e16c83148c671d7f56105ad34ce38de2454a5c6807c422b7d640e4577b718e435b0ee978fdbff427bb8a9aece830bfa5146061849d05b881c9b7212fcdf7798f773347f8b9a0d7a70a2cb83902dd84f882f7245624ba8c44b1d06f98f5159e4f51494105c0b26830321f349f0dd436d1726d00041b0a490a404a689cd9c61949cb8c1624cb83b5ef7ccbbae5fea806b2ff6d9b32c3b4e48ae0046404f2c072676a7d7922467a625948821dbc2c81b8da97d3a187b329f5e86e899fa0a200fb5cb3e083dcf0010a77a3096e3b61afa89d10c834fbd8ec58cf4beef1d1f878aa3ddfdaf747853e9fc63f9f7f2e12e84bc85e453ef19d6b4ed18958fe3405aba887287def1c8dc15777b265c0a42780acf9897858c07cc9b2b9b02b6c90fc3b77a01dd974deb58864d6e31e0ebe2574c9a9283806ea6356b71bd4262fe608740cdfc4631ec56e1c09db0bc46c89698e2eb0e55e81ef73c3c8879b25a1f362a90543c74acd4cbb35cbe9d35765405457b626d5151ee48d91ec4e03ab0429dd9feab2c461f67dfc7ec78211aec693a2815fddd400c8846d224b869565060d2f2c87b753b8df856328e65ddcd3f60638749b6943989ac46cb8e85f33228497c5d2267c75282bc7d3fc69047799cd70f359964a95667d91e13f7d30daebe8bcae1024ace8f8e551f174538b37ca05841dd4a46063b760df8e6f3630953d716c5c410a4d573152fd5ad0360d81d57a051394c739b4e64c0f90679bb3ac490541788cdfc4f6bc54cb95114f05d625530420cdaca32574e660e7f0f9862b12e4ce7efb8543cc1dd745c2a7673f526fd938ab10b82b1a9c725ed7b83573b304ee6d2c13a782368d77f770fd62aec69bf981c3db02fd32b885661ad86de36b04daa7235ca2c7a0ac6f54a43a6af26add753ca2b6991bf2b4450645fff88b7dd39e73437bde00bad496297eb4d7b8522b8c2f2d5b375f43d8b145d5f1eb5c9525f15d8f91095bc1098415496f0570ddbc02173225aeb99cea9b0a4815f7f748043799771a1439be8fbc024c7931242d48262cc08aaa3b158d7fa8a1a53658d6ff4c902b272edcac0ea9650d2956725308dd3b45924f9ac61a1333a09d1ab64b9e3e0d7722b25d972258a46640ad2fd637d28880df353921218d8dc81615f8e43547b9c9759d3c2252e3aae6f07e001483bb5b5bd8a69c0ba553292ad1d8c14af76e7ced6b19a5d2457ae64bf6dba77b17a7bdcd130fdddf49f2f290b8c667b7f2badb370c1277166fb98f700cd6dd05dd895da055a09669e3467ac5d6cfcba2872f834c1834d610436fe6dfeb22f1026a113ea69aa671af6976925cbd5dd0f99babe9b950cf89a275ef1a28c541c2f342222326b9fce88df14f5e2cc597bd120cd518c7228d6c8a8c6f42db9078b54bfbdb3d6985a5fff3878608b15d4c5edeeff9657f7ae9bb515da83879a97566a1de5ea47c8f8db7fa1bdbb0ad3e8a89806e028a8a54c61ca914dc001ffa741069ac048a0c6afdf37b308c514bdc40556ba4351b1d340cb76431035265fd4bab3cf003b2fe67f019815f28d1402e4380a89dfab1e0b90274c861b288fd381ac83f48cdc4b5218d12dc7e41c6fcd86ab860d59c960db092d456e19d0d2ccfe7233fc7baea842a410c1bfaab6fb587d6fc26880ee860b6aef390991b1b242301e28680c6d65a39d49d7c7e25a8c1411571a66010a74f87851a7314b07731bbf30c6d786ecf42709360b3fc1b12c450115f35e707b41f258073d287a602b6fd57389348b8fec9366c80350ac96b03a10b0f8ad4c09455c906b681a9a1f9405d32fd8f08fd5fa9e9ff70b2b4a0fca4cb1dd659d39e9e99c630f2d3aa056f8f3acc239497f45643e0a7b6d86e7922129429caa5d71b00a88e3aefea943d8efc66f97ac160d570d2834c8114aeb1232010ca35826faa4cf786aeda3204f74c456407ae72a09bad8eb9261510bfabe03ef8c206645f36edd78572babe7ab11f7d26cd0bbd07c868eaad8e659ff329cfa502aa61081506313f76a4f81d100fd693b37c8877c9a6b543a8ee45e826669d7326b15681f5f83af7fb13b323cc649316cbccfa93e76756718bbb0a0e11ccfd5335af216c8ea21ba41976cfd65caa7c0072bd2f2df78fe32bbfa5612057cbf2c059d946ca4f5480043ca4d79ec3f1af8d36c47b0e1cb7a01e897485b1a76c00aad500e378da9ef789b500000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x661c29", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x2388a8d8981d196c52b6b731e06991d9a502910cbaeaa5396a156b882a6f6097", + "transactionPosition": 27 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3ba", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x479cbfe", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002ce3ba19656f811a045041325820cf92e6b2348466d49f180699709bf0afcc372ea7d8543074c0b5c724a7ecb6a7582030f7dea42fec628a29ba26a0dab17df120f5f9b2740e0ca4ba1c332d86c1eea2590780970b4083f06e6772b24fde9600f285c9f542ffb9b93d15ac32a9cfaa002ed367b4d8ee029ac689e0b2a9f797a52bbdd1b01687a9ceb8c9790e00f14c1a9463b0f2caa20f079f8b4aaedd577aa0e21dd971bd3bc56a3eae10200b2e1c2270170508c50c10a32fccc9206146f1d57109fc723b6aef3a814c10ff21d9f9f57e30fb5c88eddaf18dc525847f6ac1519e200e8bfa5521554b9e6519e998b5c41b099e076efdef54142293a4164be42f89a6c006e9ca0a718ebf402aadb270d9b48bbaa30f8cc5d9558d81f61871b4fd006879593d13363fd3ef2d0a8589603037a53d315f2cadbba24eeceee4e6a5b43df62493eec2ba6c2da36e7ef21140e9d7b9f2623466cf2af2d3dac9370b4e7df3439beac4b58b9021ad7c3e8aa5fcd8d76eb21157ee86adaa6ca9fafaae6a986de700eb4446e710eb8a07d95b29f39cff7c35fa5d39f985203e26e0a739506473a53390d4a05c2a890168ae9dde4a0cfc34e7a215e16c83148c671d7f56105ad34ce38de2454a5c6807c422b7d640e4577b718e435b0ee978fdbff427bb8a9aece830bfa5146061849d05b881c9b7212fcdf7798f773347f8b9a0d7a70a2cb83902dd84f882f7245624ba8c44b1d06f98f5159e4f51494105c0b26830321f349f0dd436d1726d00041b0a490a404a689cd9c61949cb8c1624cb83b5ef7ccbbae5fea806b2ff6d9b32c3b4e48ae0046404f2c072676a7d7922467a625948821dbc2c81b8da97d3a187b329f5e86e899fa0a200fb5cb3e083dcf0010a77a3096e3b61afa89d10c834fbd8ec58cf4beef1d1f878aa3ddfdaf747853e9fc63f9f7f2e12e84bc85e453ef19d6b4ed18958fe3405aba887287def1c8dc15777b265c0a42780acf9897858c07cc9b2b9b02b6c90fc3b77a01dd974deb58864d6e31e0ebe2574c9a9283806ea6356b71bd4262fe608740cdfc4631ec56e1c09db0bc46c89698e2eb0e55e81ef73c3c8879b25a1f362a90543c74acd4cbb35cbe9d35765405457b626d5151ee48d91ec4e03ab0429dd9feab2c461f67dfc7ec78211aec693a2815fddd400c8846d224b869565060d2f2c87b753b8df856328e65ddcd3f60638749b6943989ac46cb8e85f33228497c5d2267c75282bc7d3fc69047799cd70f359964a95667d91e13f7d30daebe8bcae1024ace8f8e551f174538b37ca05841dd4a46063b760df8e6f3630953d716c5c410a4d573152fd5ad0360d81d57a051394c739b4e64c0f90679bb3ac490541788cdfc4f6bc54cb95114f05d625530420cdaca32574e660e7f0f9862b12e4ce7efb8543cc1dd745c2a7673f526fd938ab10b82b1a9c725ed7b83573b304ee6d2c13a782368d77f770fd62aec69bf981c3db02fd32b885661ad86de36b04daa7235ca2c7a0ac6f54a43a6af26add753ca2b6991bf2b4450645fff88b7dd39e73437bde00bad496297eb4d7b8522b8c2f2d5b375f43d8b145d5f1eb5c9525f15d8f91095bc1098415496f0570ddbc02173225aeb99cea9b0a4815f7f748043799771a1439be8fbc024c7931242d48262cc08aaa3b158d7fa8a1a53658d6ff4c902b272edcac0ea9650d2956725308dd3b45924f9ac61a1333a09d1ab64b9e3e0d7722b25d972258a46640ad2fd637d28880df353921218d8dc81615f8e43547b9c9759d3c2252e3aae6f07e001483bb5b5bd8a69c0ba553292ad1d8c14af76e7ced6b19a5d2457ae64bf6dba77b17a7bdcd130fdddf49f2f290b8c667b7f2badb370c1277166fb98f700cd6dd05dd895da055a09669e3467ac5d6cfcba2872f834c1834d610436fe6dfeb22f1026a113ea69aa671af6976925cbd5dd0f99babe9b950cf89a275ef1a28c541c2f342222326b9fce88df14f5e2cc597bd120cd518c7228d6c8a8c6f42db9078b54bfbdb3d6985a5fff3878608b15d4c5edeeff9657f7ae9bb515da83879a97566a1de5ea47c8f8db7fa1bdbb0ad3e8a89806e028a8a54c61ca914dc001ffa741069ac048a0c6afdf37b308c514bdc40556ba4351b1d340cb76431035265fd4bab3cf003b2fe67f019815f28d1402e4380a89dfab1e0b90274c861b288fd381ac83f48cdc4b5218d12dc7e41c6fcd86ab860d59c960db092d456e19d0d2ccfe7233fc7baea842a410c1bfaab6fb587d6fc26880ee860b6aef390991b1b242301e28680c6d65a39d49d7c7e25a8c1411571a66010a74f87851a7314b07731bbf30c6d786ecf42709360b3fc1b12c450115f35e707b41f258073d287a602b6fd57389348b8fec9366c80350ac96b03a10b0f8ad4c09455c906b681a9a1f9405d32fd8f08fd5fa9e9ff70b2b4a0fca4cb1dd659d39e9e99c630f2d3aa056f8f3acc239497f45643e0a7b6d86e7922129429caa5d71b00a88e3aefea943d8efc66f97ac160d570d2834c8114aeb1232010ca35826faa4cf786aeda3204f74c456407ae72a09bad8eb9261510bfabe03ef8c206645f36edd78572babe7ab11f7d26cd0bbd07c868eaad8e659ff329cfa502aa61081506313f76a4f81d100fd693b37c8877c9a6b543a8ee45e826669d7326b15681f5f83af7fb13b323cc649316cbccfa93e76756718bbb0a0e11ccfd5335af216c8ea21ba41976cfd65caa7c0072bd2f2df78fe32bbfa5612057cbf2c059d946ca4f5480043ca4d79ec3f1af8d36c47b0e1cb7a01e897485b1a76c00aad500e378da9ef789b5d82a5829000182e20381e802209ce6925c121712eda1a12f4df52b5a3d3105f320dc6b45f24c785776a953de4cd82a5828000181e2039220207ff6f6be1300fb4908c0daf51e3d7d0425283612424a721db42365b52ea8cc06000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x361d537", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x2388a8d8981d196c52b6b731e06991d9a502910cbaeaa5396a156b882a6f6097", + "transactionPosition": 27 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000021ef69", + "to": "0xff0000000000000000000000000000000021f07d", + "gas": "0x1afc9e2", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285068182004081820e58c0b13a7cf6ffb355576e128fb8aee41b36616a208be66d052ac70553d944dbe0f1b0acc2540132e6e73d2768ef30811b87a0fa8aafd877070dba2c069689f4ee35e6bfbaa13cbfaabf1fb4993ce394f46d869176762f568fd424489bd4f30f1393039b7e812f687b929ad1239207ac71e60dbd48891e13f11eeaa0ca96e8552f2dd050d9717c63e3a1d9e101baf351083baf7e27633b6eab89e3fbeabd50cb6e355d36fc8a22137e9687355072fefc0d1c4291d9122bb90d719155ddf81db1dd2b1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1665f4e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x3cf0131db87f6dfaeea3932633507f9dff31e2c8f7679e0eb5205035cf910373", + "transactionPosition": 28 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000021ac60", + "to": "0xff000000000000000000000000000000002174cc", + "gas": "0x180d49d", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285078182004081820d58c08b865932f85aa47714a2504c2449ae644e1e707b02cfa87f65050ead44fffdf335551663226b76a111eb07fa6f925cc1a2a3e2e0adc244bfe617b684dfa7e7e3b3b50156599a634395500da0cb12066f3376acd9ede2debe33e4c947a7c1aa710af15c15c5af34cddf7e3eb2dba604767d0ff52d42c2f96d16bb7ca5212ed5d43383279eea49778963a1fe2ef07ffd5fa66de8142d0af0bb17aaa80803d95dff6feff0b487353f7879bf9f090949d38260a4fbf6e5557bd5714ce8d4b0410bbd1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x140ce4b", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x256951a7f0727529f3eac30506f6961b9bb393ef925d1f2d92ae118855bc62aa", + "transactionPosition": 29 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000129271", + "to": "0xff00000000000000000000000000000000129273", + "gas": "0x1c8bfc9", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850e8182004081820d58c08f79c809bb284bc44d5dbea91400a0fa579c80d3a67a42aa097fd6d87f87ad57618a343124258c40eb83694be6b6fd1c8fbb50bc744e3bb07263fcf016c2b01cca117a25eb6fe38e8502f6d4f5eeb61df3e96e085f2fdf8aa1b67cc6b28de7b7165f3bb0a76cd3f86416410c209b10c43eea9cefd8dcf6da457e6d51e3921920cefc97215219538eab13d665987d0bf0856d7030ab5b71078c7b3ee750aabfba4f8a95711aedde2e313594761bdcf6e44332b66beeb9f956386f9c820a1eed401a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x17a611b", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc6547d24edca37263cea22782639104718c6ab47802a579a7c593b50c03c5897", + "transactionPosition": 30 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000024071d", + "to": "0xff00000000000000000000000000000000240720", + "gas": "0x4dea67f", + "value": "0x249707e1ba2b041", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782197b81590780b1f9d380ba870fa437208ac15878ab89d9f34543419b3e34eb4c0ffcab1dd0992538a2d9071e62187bb0ff89a3e2af7e80975593af8b408966223d147eedca680aebdd1e5bab0e28ef8339ffb595adbaf18b2d96f245274e15cfdd799db6e3a4034db1b7497bd552b7f38cd4b215e241c9988f3640325e3e4001cc21d2cd8bfcc05f2e21f8845d235d866232c134342f99eaf59c7ebc23005f57b27254bb8d7d7648a4364ad5521203158f4a2aca6046c19beb35782af2b2f3b05e4054cc5a5982fc8035f2b2e5bad7763f27a2ff9045af7149ec036d283304f6d2f24e63438b862089e498d000357170027730c997aa9882ac929ff2054cafece481a559624985d64edbf24eba8e157e7b98d14f2bf9a39a53e88862cdc4c613ccebcfb497600591a0a9018199adc75cd6d4b3c20e910d8e20aea5c1eb09609581e8ae4ab6fdeb211c974a031d6e54ce46df51beecbf879b41106c7414eb1cc5b04fda2c51a3531c7f04709c9e22f613c3d3b7ad4f5e7e781f3b42545baa78bc2493253841bcad24670ba011f5ba63fcc93e2d067f4fd0f1b614244aa5ef3841c10b7cc1a74cc81c9782d0ce1ac66177215374e7a1aea72a75e2e2bb9da9a6192b5e83f3bf67d6d965cc6a71aa5d830712576370ab75f0b7b6fc30ffb3cf538d1f9ee8447b1612b6a9c367ed3f20311c673c413c51a5a4b17bcabf9387231a5c2114b7b53f8aee48ac87a5ad6c6423c5ee5b14c54a4aa25edc9889e8246fc3b1d57dcbec40489d316220fc885f960f9a0f1b726b170a0bb338f6025eddd39e8dda03087a123f8932ffe6f36555ef77d5fac549d71a600f443ec3ad1f148037da3e1a8174cafe06935278299e757623eb47f0135f0b75b68438c64e93676fca8fdb911d7fa4768c03a37ad1301eccf1590446ab692118005081a29cfb985c9d924f5bd657bfac0e68a82a191a5ffc09122189493329c1ef4e363e9bce42884fac89c5b1e253d6ae3ca4d306ec9e2ec762950a9752d00291c5c57417006d9aba659a30d9b2b331ad8b860adc960e7597697e7c5bbf6995629d11e3c48b7b260af7a0505eada5628158b9e64913ad94aed332e8102eddad06c33064c68eb46662ea33655673f00ed13a5d5ee4f59943c4cfb1cbef7773ab83d50e98c6098e431b0a5ecd0e1a48d8740818809c0bda28094d6cd0999cf27c3166eba8f0842e81b4fc28f3e3340b3c0a321942f6d75cdd91ed3fba60b2cb7b2d5278c2f4e7a5ee24def2a35054b8d3091d554c985a2d53301b7558373ef8ce80bca45cd25e1a5357811d816a6ebc50fe673d5273453407f058311bfb9bcdd0d7d08d58a6da2ab1bcd5916ce7b2bb5ca34d875957c53712a9b1950220e8ce327d3c8a1d308dae2830620e9dc5e1f1d385fba934ff856b4627fa732ac1ddaa43b572089da4a6bee77be2cc1326fd910edc12ec05eeea0930b31806eb6143e89e024d51e1f90829f4d0189d29c414b84e0efc6a6cb9aef21257c6652c05ee743127c77d5946e48aedcb71a8ab3bb5ad4e815d882e9d397cc6d3b39644939d3085afb44969e77ff50629a356bf1caba25d9eca119afb0db5c351018d98b6b7de8d9ccea9fdd725295f738c2c8e9522d95ea2171db0c927976321c3651b67da188e509b4c64368ba8989efff9eecbaa2dd4b3e24fb0181adae49c248f6f523a5ff98a4c2d53ecc8042a8ff6863f41174cd04958fa488513cb38afb23a50f922cf247701b63ed1bf3b76d22eb64b38f39280195c59321bca0f4988251f886e16e9d6cb28eb13b1bcb33a5defe754ad4fa7554e54b9e3d1bb419f69b8e8aae47ed6e090541503f8c5ba540fc8eecd09755f1304266ebe5add110b0c10ecd0b6cc31313fb341c4beb9ff6b6801baa267f8be3f99545966ea7d36df8d78d8a7b764e2f142e7143043ba3a3fe3ad4e1172975e7e5a16920a21ed3e173b0fd4a2d178f124829c312bb34b8a086c661db0cf539643f06f9321f0ddab3443a54041dc26fef93072b60025182b4c319ceeb27490c3fb1895be83f081812a7e442f041c803636dc7bd7af7f91772485de16a3a70a6423441af2d578060152c6978b7f5eaf265f96aa593c6b8988ef6abd7f0c211685ebd15f4691d81a9b61458123e8f3222dae3d7eec64d3d793f8bcb89ff0957ca775a89e2143167d7cb39ebd4ced7f5ff6d5777e07feb6be110c0236f316ac919c2e4fc2bbab76ea38720a16a5e5b2d4bf40a8578daf93a7b78394ca172c71be8eb9d9f7872a31e25ce6ae6905b2761cc870eac7122db2f6a2462a0f6d230c3ea41b162f17567030cef113fdcde321e3b5d33a77caf6b4429ba578f598c7a76dc604661233e1d55b99359bfc5f19d67a64b6b04d36f99502fdb41c7946ac73b44c9ec05bbdcba1f76f1ecc660226fbdc1e225854866849e918e849736f04ac6c084294a822d73059f9fe80af1b41762b2fc84e140674c1864599dacf5ed6ad074e1095366a4918b0dad7abba9b24263bbf5e995ced17f759c6978d54af02a9b9c78d057896ee884d8df1fc163ef03b38d449e7f45cc41d831d132f75032c8665ce7b04b88c28b1ea61f1ace20ff87735ceac8ebff21ce2769aefd6e462c71a55150b775178016c4367d3a0772c6074ccababa6af341859176f0899d58047d4e95913a944acdca04f14c985d11118f0e9c07da419d22a1b3b249c00c61d0016ebd48c00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x924748", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbac6d7be578578bc26e152bd6d6daa4a9e87f38bdf6b42eb34a1fa0436188dbf", + "transactionPosition": 31 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000240720", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x47d5eee", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000082c8808821a00240720197b818058204fb7698ad0ac99ea2b79cfa2feaa9b606fb14a18877cfabe989d204b529fa8b958201e32feaddd3941fa6bc24f9608e4aaf72e91fb8c668ab31bd76c8a22f8c57243590780b1f9d380ba870fa437208ac15878ab89d9f34543419b3e34eb4c0ffcab1dd0992538a2d9071e62187bb0ff89a3e2af7e80975593af8b408966223d147eedca680aebdd1e5bab0e28ef8339ffb595adbaf18b2d96f245274e15cfdd799db6e3a4034db1b7497bd552b7f38cd4b215e241c9988f3640325e3e4001cc21d2cd8bfcc05f2e21f8845d235d866232c134342f99eaf59c7ebc23005f57b27254bb8d7d7648a4364ad5521203158f4a2aca6046c19beb35782af2b2f3b05e4054cc5a5982fc8035f2b2e5bad7763f27a2ff9045af7149ec036d283304f6d2f24e63438b862089e498d000357170027730c997aa9882ac929ff2054cafece481a559624985d64edbf24eba8e157e7b98d14f2bf9a39a53e88862cdc4c613ccebcfb497600591a0a9018199adc75cd6d4b3c20e910d8e20aea5c1eb09609581e8ae4ab6fdeb211c974a031d6e54ce46df51beecbf879b41106c7414eb1cc5b04fda2c51a3531c7f04709c9e22f613c3d3b7ad4f5e7e781f3b42545baa78bc2493253841bcad24670ba011f5ba63fcc93e2d067f4fd0f1b614244aa5ef3841c10b7cc1a74cc81c9782d0ce1ac66177215374e7a1aea72a75e2e2bb9da9a6192b5e83f3bf67d6d965cc6a71aa5d830712576370ab75f0b7b6fc30ffb3cf538d1f9ee8447b1612b6a9c367ed3f20311c673c413c51a5a4b17bcabf9387231a5c2114b7b53f8aee48ac87a5ad6c6423c5ee5b14c54a4aa25edc9889e8246fc3b1d57dcbec40489d316220fc885f960f9a0f1b726b170a0bb338f6025eddd39e8dda03087a123f8932ffe6f36555ef77d5fac549d71a600f443ec3ad1f148037da3e1a8174cafe06935278299e757623eb47f0135f0b75b68438c64e93676fca8fdb911d7fa4768c03a37ad1301eccf1590446ab692118005081a29cfb985c9d924f5bd657bfac0e68a82a191a5ffc09122189493329c1ef4e363e9bce42884fac89c5b1e253d6ae3ca4d306ec9e2ec762950a9752d00291c5c57417006d9aba659a30d9b2b331ad8b860adc960e7597697e7c5bbf6995629d11e3c48b7b260af7a0505eada5628158b9e64913ad94aed332e8102eddad06c33064c68eb46662ea33655673f00ed13a5d5ee4f59943c4cfb1cbef7773ab83d50e98c6098e431b0a5ecd0e1a48d8740818809c0bda28094d6cd0999cf27c3166eba8f0842e81b4fc28f3e3340b3c0a321942f6d75cdd91ed3fba60b2cb7b2d5278c2f4e7a5ee24def2a35054b8d3091d554c985a2d53301b7558373ef8ce80bca45cd25e1a5357811d816a6ebc50fe673d5273453407f058311bfb9bcdd0d7d08d58a6da2ab1bcd5916ce7b2bb5ca34d875957c53712a9b1950220e8ce327d3c8a1d308dae2830620e9dc5e1f1d385fba934ff856b4627fa732ac1ddaa43b572089da4a6bee77be2cc1326fd910edc12ec05eeea0930b31806eb6143e89e024d51e1f90829f4d0189d29c414b84e0efc6a6cb9aef21257c6652c05ee743127c77d5946e48aedcb71a8ab3bb5ad4e815d882e9d397cc6d3b39644939d3085afb44969e77ff50629a356bf1caba25d9eca119afb0db5c351018d98b6b7de8d9ccea9fdd725295f738c2c8e9522d95ea2171db0c927976321c3651b67da188e509b4c64368ba8989efff9eecbaa2dd4b3e24fb0181adae49c248f6f523a5ff98a4c2d53ecc8042a8ff6863f41174cd04958fa488513cb38afb23a50f922cf247701b63ed1bf3b76d22eb64b38f39280195c59321bca0f4988251f886e16e9d6cb28eb13b1bcb33a5defe754ad4fa7554e54b9e3d1bb419f69b8e8aae47ed6e090541503f8c5ba540fc8eecd09755f1304266ebe5add110b0c10ecd0b6cc31313fb341c4beb9ff6b6801baa267f8be3f99545966ea7d36df8d78d8a7b764e2f142e7143043ba3a3fe3ad4e1172975e7e5a16920a21ed3e173b0fd4a2d178f124829c312bb34b8a086c661db0cf539643f06f9321f0ddab3443a54041dc26fef93072b60025182b4c319ceeb27490c3fb1895be83f081812a7e442f041c803636dc7bd7af7f91772485de16a3a70a6423441af2d578060152c6978b7f5eaf265f96aa593c6b8988ef6abd7f0c211685ebd15f4691d81a9b61458123e8f3222dae3d7eec64d3d793f8bcb89ff0957ca775a89e2143167d7cb39ebd4ced7f5ff6d5777e07feb6be110c0236f316ac919c2e4fc2bbab76ea38720a16a5e5b2d4bf40a8578daf93a7b78394ca172c71be8eb9d9f7872a31e25ce6ae6905b2761cc870eac7122db2f6a2462a0f6d230c3ea41b162f17567030cef113fdcde321e3b5d33a77caf6b4429ba578f598c7a76dc604661233e1d55b99359bfc5f19d67a64b6b04d36f99502fdb41c7946ac73b44c9ec05bbdcba1f76f1ecc660226fbdc1e225854866849e918e849736f04ac6c084294a822d73059f9fe80af1b41762b2fc84e140674c1864599dacf5ed6ad074e1095366a4918b0dad7abba9b24263bbf5e995ced17f759c6978d54af02a9b9c78d057896ee884d8df1fc163ef03b38d449e7f45cc41d831d132f75032c8665ce7b04b88c28b1ea61f1ace20ff87735ceac8ebff21ce2769aefd6e462c71a55150b775178016c4367d3a0772c6074ccababa6af341859176f0899d58047d4e95913a944acdca04f14c985d11118f0e9c07da419d22a1b3b249c00c61d0016ebd48cd82a5829000182e20381e8022004dd1fca6c24f41664d592050ba2076cd6039cda423fda286e09f819521aab4dd82a5828000181e203922020077e5fde35c50a9303a55009e3498a4ebedff39c42b710b730d8ec7ac7afa63e0000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2ed1ebd", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbac6d7be578578bc26e152bd6d6daa4a9e87f38bdf6b42eb34a1fa0436188dbf", + "transactionPosition": 31 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0792", + "to": "0xff000000000000000000000000000000002d0798", + "gas": "0x4925b8d", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821962a95907808265626909a4ea50de70b5e4dfaeeb5b403aab71f3409491deb25fb3c4449f50fe2bc1a5537e4b2608e03546eaa9d29687514caf1220dcb30c1e835079e713548f6740668c61e52d683dc8b67070abd42b15a531ad8b40175096f86a2c8b2ec702e738164a93493f95163c8c21e7e52868bf0fe9c5b00f8990f3f7ceada05f43383b91396b367551323e598bbdab72e18520442cea081a512f194206860173141bde8125f3e07bb1cf4d5fd549d25a0d4c88ef18b36556d3bc244e193bdd09d2875d98ede100bede4df18c21c96fb3cd453a1f3f93a0fdef627cca54d991ca62939ab2da07ae3aedc77fc02f7adf0bc2ab8fc7a2dbdbbe43822a6614121339caa742d899a8cec7b24bbecb74223453a797fb13d2a02fa5d9e64bea767015e9b204e07f623f6a3fa48d963e1862f6b3621aace85bcf136cfefbd90d7da18acd89a80095c8c5d69e81fb87509d591346c2b64a4b00034d547ae2a2434e2581d941e66b13ee33c24e4c6855b89e714903fa8144a973bb15845b611cc4b35a31b9af98a9dc2008efd63b66010cce307a8d0e0cb4ba546696799bc982100eb2a084d8ba0d2298be4031ee9be274ac56c4a54180db55f3c7d2a08ef23d62f32b77464cff82a20d207dc7d77088d48ac3be042b89b5474e5d3e01707df11c3fdace110f092303e87dbbf1fb68525eff8e9c6e3d2c707b1607df2825f9185347c8ea30bee636482051aac9a3186db603d47dc197b7f6b6641f8f04dc677a706ef7612b0319e64fc6054c264d352df72a8d0e5215e01b5e8df18a5c57f858ee2551ed283394b8a9ba8af2f2e5e2893af0b1eac66f7392bc5c03db6dd4ca92b79a14376f4708a9e02d2754869a5fad8c2053d04c23870d894b1f544bc24c52317b96c2cb3c48819dcef816df07139213c497e0ab81161b3edf503f2b0cadfff3a50ea9dc330014d31667bcaed035df0baf2e5f61d030c0990cb0632297193fd1388b3e1e92e14dfc50a3ba223014083e1b5b0dc441895eb44fb49fe1c05d0fae11eff6345c78a0f5ec196853cd23491bce4f19d4ca991243608a435e5237a4f42e2809912c934374d56394f5f43be0ac3849a53b5469ceed1885089c11cb32249426f9783e01a233d8bb9214b2156bd17e7e2bee7bb38520aad22aee9b7c1632703135717bba0f2f96df3af123c738c90bd19d8965bb21530f87ee8e8e0d3d1b080c456f910c499416cf83efa02a2f15f1a8fb49abc46f09456a809d1cb602dd97c45c8f35b1cd44c41a030d883d30764fd55953aa8c9d01a706cffea8a758bde4b13eb25338eafced8bfd2cdfe50d008a55849dc87554ad043e4598700c1c8909f1fe87ada76c4928a51c4a1cc4a70d4ffce0b1f0ab0aa1a71c8d8f59d3b1fb23a3293b55984d2f0bc0a9fbd1b5c2aecd8c50bd298d17f3940f287d128e62e09785a742f37ea7dca45bdb4e4aeb1885f4492f87799a30600051f8246b7b51fa9c67b2862019515e8bfc96177d185c55c69650e9fa4109ca4118f54cba2d2c425b4a59b60af44bcc0d183103185d7018f4e7835311a615e4162e6988e8722c752a221a87a4821fc2b7347cd18953e62a394972f913aea13a7af23a080800aee625613c3e82a45b1b788e023e6954c2f01d1cf0e6036c9a2ccf3e62c76eaf4795ff9f2546ae0347681aada949ac906bd3a1de1e07a9a62edba95110aeefbc36681894891a4a834826ff1a83cef05e707616fc89690fff9968798fbda3a53d84ef6c8a1a14e215ba888759a872c6ce00a5574c6de5eaabf613c83cf931a259230c371f0d99ec4716b8ef8901a254b12960c073f3dbdda8f9cab5a901cd8c0ee25127a43497f48e7d137d9113e3e3e6703db8aa3ddf0259121a747e3af4eaa99bd81bc89e8f01a9ec7ffef3d16003bfa686999882c2f04945dc1a23af1ee3454e20af05849310a51820069ad852446beb0315bc621a37a20db633b68659b4efc5e9dfcf9633ecc623d691dbccea398ec18581df9172eb1111f374974f1f455833dea61076684404d22b455a0cda3dd693170362e5709df55aabc08e74bd72bc667864f7d1e43d8ae4ba7131cc95584d0b0fd50e4468be82a9ecdbd22e196d47626ddc3aab7ed44f075e6b75123877609e5ccb0f06e880927d1d7ef767abb799e76335a6731719a0c45b970919652ec1345c6aeb3cba067490778aa13114dc3ace9e9c344059e00a41138130670c8aaa8e9caeb84bfd93ad31f17c8f8adf749f9952dc66992720ed9a7c418b7997c6b7333d17d625156c3401379625a57b221291f34b70d860cf19f7ab19fd6f368e5f8a5b6ae9cde87e77036e3a3419868e9da66c4586a15124dfb129a843552fb4ee6b547a330860e4853c1e6756166583bad1cbc0f1237b2cb83b3360b0e4cc99f573fa80b8ff45553e90ebf37e81d7e5e607e0588938e7688c35c074b450f658b799876b7e747f916e4b34e5c528ead2f62bd8b5fed53a3109018120998b9ed887ecff9f3b4f9bafb07aad375e2932572cc4dbfc74eb914a9312da02e733d82a0ee714c267539a6b098d2b4b3d5c72a78b94cdab5bfc68c211d416527daf7ed999906bb2ba6d6a12f721ac588d2ef19aa32ff39a3911e16934a67666bca132a0e2cef9145053becfa4cb0998ec5a101d15276d79029516d3091078d61fc8f4ac3a6329bf79474af2680c599507a9682f2416968e14b2d39b00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x97deb7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1c497ac8ca436d3bdf10d78a0b639383046a81cbe079c6af5939c4e235a8cfa0", + "transactionPosition": 32 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0798", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x42b55c1", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002d07981962a9811a045b28e258205e3d639571a49f742b43f2f1c00218f4564577b58a3472c5e3d98a8e0d7838ec582019705c311b3d19f529f3c2161e7b3a08ffba8b20e0a434e7dcf4c04ea43f79345907808265626909a4ea50de70b5e4dfaeeb5b403aab71f3409491deb25fb3c4449f50fe2bc1a5537e4b2608e03546eaa9d29687514caf1220dcb30c1e835079e713548f6740668c61e52d683dc8b67070abd42b15a531ad8b40175096f86a2c8b2ec702e738164a93493f95163c8c21e7e52868bf0fe9c5b00f8990f3f7ceada05f43383b91396b367551323e598bbdab72e18520442cea081a512f194206860173141bde8125f3e07bb1cf4d5fd549d25a0d4c88ef18b36556d3bc244e193bdd09d2875d98ede100bede4df18c21c96fb3cd453a1f3f93a0fdef627cca54d991ca62939ab2da07ae3aedc77fc02f7adf0bc2ab8fc7a2dbdbbe43822a6614121339caa742d899a8cec7b24bbecb74223453a797fb13d2a02fa5d9e64bea767015e9b204e07f623f6a3fa48d963e1862f6b3621aace85bcf136cfefbd90d7da18acd89a80095c8c5d69e81fb87509d591346c2b64a4b00034d547ae2a2434e2581d941e66b13ee33c24e4c6855b89e714903fa8144a973bb15845b611cc4b35a31b9af98a9dc2008efd63b66010cce307a8d0e0cb4ba546696799bc982100eb2a084d8ba0d2298be4031ee9be274ac56c4a54180db55f3c7d2a08ef23d62f32b77464cff82a20d207dc7d77088d48ac3be042b89b5474e5d3e01707df11c3fdace110f092303e87dbbf1fb68525eff8e9c6e3d2c707b1607df2825f9185347c8ea30bee636482051aac9a3186db603d47dc197b7f6b6641f8f04dc677a706ef7612b0319e64fc6054c264d352df72a8d0e5215e01b5e8df18a5c57f858ee2551ed283394b8a9ba8af2f2e5e2893af0b1eac66f7392bc5c03db6dd4ca92b79a14376f4708a9e02d2754869a5fad8c2053d04c23870d894b1f544bc24c52317b96c2cb3c48819dcef816df07139213c497e0ab81161b3edf503f2b0cadfff3a50ea9dc330014d31667bcaed035df0baf2e5f61d030c0990cb0632297193fd1388b3e1e92e14dfc50a3ba223014083e1b5b0dc441895eb44fb49fe1c05d0fae11eff6345c78a0f5ec196853cd23491bce4f19d4ca991243608a435e5237a4f42e2809912c934374d56394f5f43be0ac3849a53b5469ceed1885089c11cb32249426f9783e01a233d8bb9214b2156bd17e7e2bee7bb38520aad22aee9b7c1632703135717bba0f2f96df3af123c738c90bd19d8965bb21530f87ee8e8e0d3d1b080c456f910c499416cf83efa02a2f15f1a8fb49abc46f09456a809d1cb602dd97c45c8f35b1cd44c41a030d883d30764fd55953aa8c9d01a706cffea8a758bde4b13eb25338eafced8bfd2cdfe50d008a55849dc87554ad043e4598700c1c8909f1fe87ada76c4928a51c4a1cc4a70d4ffce0b1f0ab0aa1a71c8d8f59d3b1fb23a3293b55984d2f0bc0a9fbd1b5c2aecd8c50bd298d17f3940f287d128e62e09785a742f37ea7dca45bdb4e4aeb1885f4492f87799a30600051f8246b7b51fa9c67b2862019515e8bfc96177d185c55c69650e9fa4109ca4118f54cba2d2c425b4a59b60af44bcc0d183103185d7018f4e7835311a615e4162e6988e8722c752a221a87a4821fc2b7347cd18953e62a394972f913aea13a7af23a080800aee625613c3e82a45b1b788e023e6954c2f01d1cf0e6036c9a2ccf3e62c76eaf4795ff9f2546ae0347681aada949ac906bd3a1de1e07a9a62edba95110aeefbc36681894891a4a834826ff1a83cef05e707616fc89690fff9968798fbda3a53d84ef6c8a1a14e215ba888759a872c6ce00a5574c6de5eaabf613c83cf931a259230c371f0d99ec4716b8ef8901a254b12960c073f3dbdda8f9cab5a901cd8c0ee25127a43497f48e7d137d9113e3e3e6703db8aa3ddf0259121a747e3af4eaa99bd81bc89e8f01a9ec7ffef3d16003bfa686999882c2f04945dc1a23af1ee3454e20af05849310a51820069ad852446beb0315bc621a37a20db633b68659b4efc5e9dfcf9633ecc623d691dbccea398ec18581df9172eb1111f374974f1f455833dea61076684404d22b455a0cda3dd693170362e5709df55aabc08e74bd72bc667864f7d1e43d8ae4ba7131cc95584d0b0fd50e4468be82a9ecdbd22e196d47626ddc3aab7ed44f075e6b75123877609e5ccb0f06e880927d1d7ef767abb799e76335a6731719a0c45b970919652ec1345c6aeb3cba067490778aa13114dc3ace9e9c344059e00a41138130670c8aaa8e9caeb84bfd93ad31f17c8f8adf749f9952dc66992720ed9a7c418b7997c6b7333d17d625156c3401379625a57b221291f34b70d860cf19f7ab19fd6f368e5f8a5b6ae9cde87e77036e3a3419868e9da66c4586a15124dfb129a843552fb4ee6b547a330860e4853c1e6756166583bad1cbc0f1237b2cb83b3360b0e4cc99f573fa80b8ff45553e90ebf37e81d7e5e607e0588938e7688c35c074b450f658b799876b7e747f916e4b34e5c528ead2f62bd8b5fed53a3109018120998b9ed887ecff9f3b4f9bafb07aad375e2932572cc4dbfc74eb914a9312da02e733d82a0ee714c267539a6b098d2b4b3d5c72a78b94cdab5bfc68c211d416527daf7ed999906bb2ba6d6a12f721ac588d2ef19aa32ff39a3911e16934a67666bca132a0e2cef9145053becfa4cb0998ec5a101d15276d79029516d3091078d61fc8f4ac3a6329bf79474af2680c599507a9682f2416968e14b2d39bd82a5829000182e20381e802204c7ab7b39b702b0f85ccccf1ab3be3cecfbcca6270f318e87139e54034536273d82a5828000181e2039220203e1051280d09281ef100474edd98804234de5c26e1f9b4f2fe44fc6b0f764b04000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2f02cf2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1c497ac8ca436d3bdf10d78a0b639383046a81cbe079c6af5939c4e235a8cfa0", + "transactionPosition": 32 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0792", + "to": "0xff000000000000000000000000000000002d0798", + "gas": "0x526a0ff", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821962ba590780ad996281f9ca4a29d9500e079f0250a570058f543c381fd8ff5207dec9eb29a21cf732e00a9eb5fc8ce8b92c68151f3ea5a8dcee1ce14acbc24ae6bf934d64e15815bc9f77f315e05c00432ebbadd65e756658ac51f723ab9fbc7f18d4ddef6b17d65d1d29f5c20bf130c8961f470eae522d4d7d4dc28fd743b26f887c9a9839228f2aa8617759e82bbf05b981dd0c2b8445502cb8b61250cb5b8d01c79b033712bd91da570437900ca7f3162e50b1761db4a01a33a1772e8e7650754c3f6a3ba979537cdf00b7754517d53fc5e18d882ae7117ba605ccf1d721e3ea5140a73690ee33308bf5a4fe2cd26c63ab034bfeb90f7df81b484f729a56493faf1040f97c2c85ec1020fd161cf9e0c8c1c065bfe31807b8784d5fd0f68c698f91229f700987a0ce6a73bd01fc4d460a72cfdd968493b612fa80e40a57b149e549d1efd97db9c8d1536af9b1e70fba2d395c7d50a514b2278efb333df538fe508f53d4cc292d2af34a41caf2eaa162222154d676730b10b10f9d118a064b8315418de4dfaaa8676c0d845e271c4f6d2f0a39db3f400434354a85d5fc8bc2bf7809da7623eef18779dfafa0e608c00871be55b43891059115ebbaddecb8b8d2862999e8b8803111718f8a23effe8ea2d666bed91ee98a3c36d6b89c9c90faf1576703b1e90b9b4164c1ab5c4daee51b26f1e556b4c2fac68125c293a77808e780c08bcfff3939ebd5eec5effa30f49d5860ca1bc890f610404430af070a43862d00ffad5de51d8701138d0cc46b856104fd792a08ba470d441992e282877b7ccf31844480ae2b679357e12c96b95620b64bcf38b27944f9ee4bf8768c9966973d3f40f6575e45a2dec3509de997e50445251a16608f299e2d8b3e71618942a15fc35ea5eb48da6a6600f1fffbf7e5b953b27f857e9773ca7ffc9dbc14ec275e60471c2bd60d07b74585bc5a34b93f36658600b57bcd4901d436374891e76826790619d555ca2dd736aedc7150f3f237345e77508e8dc3dc4310b36cad0af9e65a7a105c4b688eeca266d381d70f589348de350d0f19e7560a321282e17f954a6196322945b60efc03e30afef8d77d3fc1dd2b37f0d412d070edeed492eceabc80e650839f7b0e35ca378c689644dcc4245e08fc2fb1b6005fcce45165107ef7595ac4238af11527076d10e0fe0d0f09e7cacf37a60d9f7ed0259573911c85967b143e472b1079e14ce53f8bc32c7379a667792baca4475d11ffca642f2d780b1d14fef9e9334679d2a12e88b20870a1b7c6320ea9825b4e43cb92c048d3e2380f28232eaceac0872bc1f7a105fa8791d91832003b25e6cc7a9c7b4aeb3110a2a5ee2c973eb5a80b91a73fa6f229913b4105fba4130b66fee5180beca7baa641a2c4e5c2e6cf634bc48d90ac29df2255b199ed35668aaf0da3623882bc8205f2ca636ca57d421362ecb5ef30116a0116cd11ed69d8370738c0189ffdc4643ccda19121bd491092f5ae8a4fc41011c632a77ccd63ce37ce06ea0b6f1fe85e8708752916d3e4452933112c24c0cfdfd277d6f8380faba68b7895890a935554aa91a5b36e1530c9cf70dcece8ae88e0c5a6a57393b525df9776e959c1e2c3dde4a29bf9fb75fd97c09aeffbe6db0470e4a2643e67a3b853a8b316a486746132475b086318fb4544aa997ea1b029277e735e1b5594c143811c2b516042ac4dc03ca4fbe35b8ebaa09a29077e3781855a0c8d2145fc8265ace6acd485b54d45637233c4b802c20909601743f4ec91a511dd1c9a40151c1cee99ace0e96fc9899a95167bf257a296870409f20db7cf21d196c17d3b129a77ac87501ed12703bd1f3099f7c1a13522148184103faeac28ab678d0f0a147ecf4df1f500be9247273f7509964b9030579626584f64f121fa58c2553f3f9b4ce40f222fd6f0e6ce54474094db8dedd92f9fd120ab7e9ac7cd8b5c54380cecbf1aaed48e2168c1d6103c663abbadc136128e5ea5d92a9f20b9829c623d416a7bf3fafefacb698389f86beb29622dcca8a1196f72bfd67519413bbd81d5301435d8a591acbb2ad327ecec80ddda5c4a5ab366565db6b8c5fe50e2149a23d54c3969b397ca76981b100d57242378214addd615783e11d4be7c9581bdedafe3ea2d75960bb1f45337e4a8468ad1392e72b77bb73f9f72878441834c67488d6d094731df676f4f51ec0deed3c2d0669a02accd31a7bedd6fa9832cc62038aaf19253b18e9fbdacd86b15cfef36e9a6d23de4ff82b8ef728903b755295073b6c082b64793278b973a0c3059ace8e6e374fd0a4c0c81e8c40acfb96043d56e361c1c51542d0eb119cb806c19291c07e70e5ec730c251c32dfde23d16db87e8abc640fe068234cd9cc8327c68102028218fa8e804694dce870992a7a0b93763c76da0dc8973872c1bec2eacf36350d6fc6ce3e83380539c5c28ff24eb52827157e585468cc0a89a934dbb6b15c5bc889979f6ebddc9fb7e1fc68d5ddeacf15de8fa67f741acb10f452e47d04592810872d8d48ca85c4aea240fe825d6ccaec9faac8ba8a8b91d60a645e0656da4c0dc0cde1bcd1218c301e16881f782d91a57aa5e063c03762184110973924c9ecf4ce91c56691500f9251d3e528dd75bdde769d4f8e04aa7330362cf1bf2f1029ef25f8eb27059fb9d30d438d77cf63f364baf4744d243acb5966b79c5c35f74e8695c5b58469d00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x97d42b", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8ade285038462b2ece31e2ee0ff1af1448e9cf8e07520f04618b5b26436a73af", + "transactionPosition": 33 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0798", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4bfa5bf", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002d07981962ba811a045b2bd658200614498463ce77b23bbf18bfd324b0fd4d5b102634806020ccff3f8c78f955b158208eb133c33fa726942d694d350b629cf534788e290162780485829b4a70160907590780ad996281f9ca4a29d9500e079f0250a570058f543c381fd8ff5207dec9eb29a21cf732e00a9eb5fc8ce8b92c68151f3ea5a8dcee1ce14acbc24ae6bf934d64e15815bc9f77f315e05c00432ebbadd65e756658ac51f723ab9fbc7f18d4ddef6b17d65d1d29f5c20bf130c8961f470eae522d4d7d4dc28fd743b26f887c9a9839228f2aa8617759e82bbf05b981dd0c2b8445502cb8b61250cb5b8d01c79b033712bd91da570437900ca7f3162e50b1761db4a01a33a1772e8e7650754c3f6a3ba979537cdf00b7754517d53fc5e18d882ae7117ba605ccf1d721e3ea5140a73690ee33308bf5a4fe2cd26c63ab034bfeb90f7df81b484f729a56493faf1040f97c2c85ec1020fd161cf9e0c8c1c065bfe31807b8784d5fd0f68c698f91229f700987a0ce6a73bd01fc4d460a72cfdd968493b612fa80e40a57b149e549d1efd97db9c8d1536af9b1e70fba2d395c7d50a514b2278efb333df538fe508f53d4cc292d2af34a41caf2eaa162222154d676730b10b10f9d118a064b8315418de4dfaaa8676c0d845e271c4f6d2f0a39db3f400434354a85d5fc8bc2bf7809da7623eef18779dfafa0e608c00871be55b43891059115ebbaddecb8b8d2862999e8b8803111718f8a23effe8ea2d666bed91ee98a3c36d6b89c9c90faf1576703b1e90b9b4164c1ab5c4daee51b26f1e556b4c2fac68125c293a77808e780c08bcfff3939ebd5eec5effa30f49d5860ca1bc890f610404430af070a43862d00ffad5de51d8701138d0cc46b856104fd792a08ba470d441992e282877b7ccf31844480ae2b679357e12c96b95620b64bcf38b27944f9ee4bf8768c9966973d3f40f6575e45a2dec3509de997e50445251a16608f299e2d8b3e71618942a15fc35ea5eb48da6a6600f1fffbf7e5b953b27f857e9773ca7ffc9dbc14ec275e60471c2bd60d07b74585bc5a34b93f36658600b57bcd4901d436374891e76826790619d555ca2dd736aedc7150f3f237345e77508e8dc3dc4310b36cad0af9e65a7a105c4b688eeca266d381d70f589348de350d0f19e7560a321282e17f954a6196322945b60efc03e30afef8d77d3fc1dd2b37f0d412d070edeed492eceabc80e650839f7b0e35ca378c689644dcc4245e08fc2fb1b6005fcce45165107ef7595ac4238af11527076d10e0fe0d0f09e7cacf37a60d9f7ed0259573911c85967b143e472b1079e14ce53f8bc32c7379a667792baca4475d11ffca642f2d780b1d14fef9e9334679d2a12e88b20870a1b7c6320ea9825b4e43cb92c048d3e2380f28232eaceac0872bc1f7a105fa8791d91832003b25e6cc7a9c7b4aeb3110a2a5ee2c973eb5a80b91a73fa6f229913b4105fba4130b66fee5180beca7baa641a2c4e5c2e6cf634bc48d90ac29df2255b199ed35668aaf0da3623882bc8205f2ca636ca57d421362ecb5ef30116a0116cd11ed69d8370738c0189ffdc4643ccda19121bd491092f5ae8a4fc41011c632a77ccd63ce37ce06ea0b6f1fe85e8708752916d3e4452933112c24c0cfdfd277d6f8380faba68b7895890a935554aa91a5b36e1530c9cf70dcece8ae88e0c5a6a57393b525df9776e959c1e2c3dde4a29bf9fb75fd97c09aeffbe6db0470e4a2643e67a3b853a8b316a486746132475b086318fb4544aa997ea1b029277e735e1b5594c143811c2b516042ac4dc03ca4fbe35b8ebaa09a29077e3781855a0c8d2145fc8265ace6acd485b54d45637233c4b802c20909601743f4ec91a511dd1c9a40151c1cee99ace0e96fc9899a95167bf257a296870409f20db7cf21d196c17d3b129a77ac87501ed12703bd1f3099f7c1a13522148184103faeac28ab678d0f0a147ecf4df1f500be9247273f7509964b9030579626584f64f121fa58c2553f3f9b4ce40f222fd6f0e6ce54474094db8dedd92f9fd120ab7e9ac7cd8b5c54380cecbf1aaed48e2168c1d6103c663abbadc136128e5ea5d92a9f20b9829c623d416a7bf3fafefacb698389f86beb29622dcca8a1196f72bfd67519413bbd81d5301435d8a591acbb2ad327ecec80ddda5c4a5ab366565db6b8c5fe50e2149a23d54c3969b397ca76981b100d57242378214addd615783e11d4be7c9581bdedafe3ea2d75960bb1f45337e4a8468ad1392e72b77bb73f9f72878441834c67488d6d094731df676f4f51ec0deed3c2d0669a02accd31a7bedd6fa9832cc62038aaf19253b18e9fbdacd86b15cfef36e9a6d23de4ff82b8ef728903b755295073b6c082b64793278b973a0c3059ace8e6e374fd0a4c0c81e8c40acfb96043d56e361c1c51542d0eb119cb806c19291c07e70e5ec730c251c32dfde23d16db87e8abc640fe068234cd9cc8327c68102028218fa8e804694dce870992a7a0b93763c76da0dc8973872c1bec2eacf36350d6fc6ce3e83380539c5c28ff24eb52827157e585468cc0a89a934dbb6b15c5bc889979f6ebddc9fb7e1fc68d5ddeacf15de8fa67f741acb10f452e47d04592810872d8d48ca85c4aea240fe825d6ccaec9faac8ba8a8b91d60a645e0656da4c0dc0cde1bcd1218c301e16881f782d91a57aa5e063c03762184110973924c9ecf4ce91c56691500f9251d3e528dd75bdde769d4f8e04aa7330362cf1bf2f1029ef25f8eb27059fb9d30d438d77cf63f364baf4744d243acb5966b79c5c35f74e8695c5b58469dd82a5829000182e20381e80220de75946bac8d0de4cc507110766a5f17cc19b30c8d0b0a55cf1b2b9afa65c939d82a5828000181e203922020d3ed4d594376bea8a01dd6414cca63e8f5165f21ae5d3ce86714ccca0f03f504000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3662a3c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8ade285038462b2ece31e2ee0ff1af1448e9cf8e07520f04618b5b26436a73af", + "transactionPosition": 33 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce375", + "to": "0xff000000000000000000000000000000002ce3b6", + "gas": "0x32202fd", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708195c92d82a5829000182e20381e802209d8f62a071341cd01d80033faa7c9ef07f7e1d8fbe534b0f811772b3fda0b23f1a0037e10b811a0454b28f1a0047eb4ad82a5828000181e2039220203c71b16224218e29bc29672e8db69ba0c012113a8b5d8cf2220c261f2a0b4a3d00000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x222e801", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0ab1e62d5a67bd84621c1419f136c59115731ca47ba1da49e589760d38c95b84", + "transactionPosition": 34 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3b6", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x3169cd6", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0ab1e62d5a67bd84621c1419f136c59115731ca47ba1da49e589760d38c95b84", + "transactionPosition": 34 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3b6", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x305d78e", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0ab1e62d5a67bd84621c1419f136c59115731ca47ba1da49e589760d38c95b84", + "transactionPosition": 34 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3b6", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2f3c21d", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a0047eb4a811a0454b28f0000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x489dcf", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e2039220203c71b16224218e29bc29672e8db69ba0c012113a8b5d8cf2220c261f2a0b4a3d000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0ab1e62d5a67bd84621c1419f136c59115731ca47ba1da49e589760d38c95b84", + "transactionPosition": 34 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce375", + "to": "0xff000000000000000000000000000000002ce3b6", + "gas": "0x4abaf3a", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782195b555907809884d69eeaf1026b549be04d95a7ba703a48494ebdf41d17bc31dab9d7c1a4eb9b8cb0b88cd2e55617338b8c5a58d8feb896df6dfbac66c6fbc56fcc0f17a40fb03339bdbeaa70f0c1de65480e3ddddf1ae71d64dedfd200c263962df45b20761265f747786fac48c4e890695c371113ec4175b2234fc81296ac9116c9601332b99d0206321e58b16bb0d7a69e49a26a96ee1f0b811031d79fd51dd6d0e4498cc7f65e385df4287d72ed0a4a4dbe019a545abb5ebb5f170058a18293f41ed1228286e4110fdf37e6e04aaa156bcff40c7038ed380fd8eaddc864faf7813ea3349785566d1f911b529990df437c2a4f929622ce6571923fb7087485996562785a046a44d48847dd949522a274d3fbef4bf000b9c9979d64df5d77c0efa6c5e4680e457e02379f7d9da2a35de7dce57aecdbc696d4f2cc02d802117a371bd456af945be6d3e9e2146b25c30b7da50ce4dea9055fd1c429ed8ff3b6a7d14f16fe20d8f9ec8802abd3b8f61083c1f05cf3c31369d29938ab21e1a0aefc1f5f2eef4c8ccf3cd9d89a930697c9c9e0042ac63288dc70b9bd5d86e36fee93d5e2eef36a89e3db13cd6be53c37649a0e287ad71caaa0cd08dc46b5c93952d6a9540408017688facad9f4e7c94a24f80a7e8abe7352222a51a82c80a5cf3b0d11f1b071d20b5c67f2c120b8a7f722abc21fab82893fbc206501e4a5b8fd3e881ab39b89ec931bbf3dfc2f9ffa139c9a518552ffa4b9547d88a53cc7947f6c8145c33ae6feb9bfa3d7ed81a7df09827e16033ef88d6990fa158a56750d1f554de9786bb2fa83fb593f11b7747633ac9978c6991256e5fb0ca9a7a70b26fad4d27baa338e8eb3ef2cc3321361a7b44b84b5a5093dc6a04883c5843d2125797cf4be06297cf765eab5b11a2d2c5f3ade90a787ea41b236e1c4291bd547de7c16ed4046576ba515ba1786d16a9d7e55ba802c43702e1a7f4eb842b4a0a96772ed3ccf9a8d53283d5f2a31fde784532cfe8cff41802a72a3255f6998ab70c87afe02271b5ca1ac137350f68636af6a540f0bfaccf2074b0e3b562d7aa01af153d118c2375f053e916152723a1dc3bbc16c0d8fcf9cd2e494f71540f0610e12a6f871a3848b4b96fb73d259e4108ce519dc0fb10ebf6a83aa9fb407a9578cb12ae2eebc43362358255c093c66dfc22d6a2340d292b41a6a303c116579537134ee370eeec1ba522f131d702130b5f3a49defa3941176ad0789abeee3becc738ecea0140ef69f574cf0bfa214a9a4a9bae8fc78831207cd8784c917b967b502ab4a36fa814aa55e50915ce6adf77fba178c69d94ccd5b949a7af66e74de04232563e2fdef2338b83cad860e8b29259888c35159f8e64bfa34ddd87b32450c04770f20e6db09c137fb9016382987673cd00c284cffc207b47c83f6d6e290c287d4a1567d94a012043c4a51d3403467b4a21a7c6276f7239dde0966df4c194be5951b3c2f99480ab17f11463d9625bf27656d0b8285d3dadbdd73ecddaae62c825f621bdafcc7af4370d13be788e472b2975d7d55689341f9baa6e149e46b6826ca829a349172faa749f97f3d6ecbb7af40de6b28129aed0ff96febce04bb4ee7be532bda99fbede436a027b1c665bfa7675607fb57a5372ad9152ce057aa429ddf38fa3c9bdd8fd6206c49ffa21fe8f295eba5f54ab9fa4d609075877e8502fca77deaacc884564262bb091c2673a1ec8799887adcfb4e704127113eafa4c3d9ff3e65999049c92d2807c3799be3393ab73d781f5b8e7c20ec0c06428765120f54fd38e51b4d1e1c20db2e34f516ff69206330cb7acbca9eacb1ca8150b1cdc5389e70ffdc7d51957acef211a7eaa79fd9f77386681dc5e38f49dfc57f3825574b15b15656fb0945c7a681fa170a9710061fd7caf7abaa47cae606ac96cb7f1fbe0068c2e8f921be971d95596059a93fb8deeba5d4a62cb47e8dc1dce320e84038526c7c060e9d417f76f0f73bcb76f75c645230e5a90700c828274e2014c7518d2e1ddd46aa16c747153524b5e96cdebcb298c811e54753b3f43334537d058ae0dd56e20305133cfa46a5cc7006fbbea66a8b984cc8544ea5a76f0c2ed9791bd26ea64647d4eb00661f3e95693b61f2bc7ff947076ede2bb5091c147ab4909cb5621327875ba61e68975ee5e03d45e8ddbaae869db24ff4f70adda4dbe7172e4b36c6964a0ace19a86e08fb21568608cd852cab0449596ce4b9e49db0cd51a50f2b1ca224b9c96b8c1cd106aeb48c98ef00715f28fada0f7ba49177a3d335b7d009cbd88d6efefa71054423c1ac2c035ccea176b1042ee93168ca564503be614e44cabe8e122f183ee2cd6f49ad5413a4baa3213bc12c22518c6e87dcc93894b68ae158744f9f8e6d8e85a2bd79d8ffec811994db429726802997f02547c9adc375c68b458b373e9b8f78a7929fe22894ac92778dbc2efca07d79ff57b299a85fe25de4a6d01408bfe07dfef673cee97649565b439306e36bafc9baa89916b7ddb6dee4fc1912aae243fa0aef0c2b63e1850f22049737a5f8d2dfd273da4036dcfb1e9bcca5afa48401ac3823b00b4e5b16793a859abdf9da123b1ede0a42cb2c9005550a95039882814e58f6baa6311999faab45021217ab8b95e78306b6a172ec0c8ede1d7ce86879ff48851d8f8b03d000d04532155db191b8368833e7917bcb257ffcf64f821b00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x699ac9", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x5391369997f0d1335d275d9f74b26e1b6633b7949375e065d75ecf3e9ee1b478", + "transactionPosition": 35 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3b6", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x472f623", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002ce3b6195b55811a0454b2585820ce4b423b5779ab9911bd426632a925b6a83466ce6e076590b106211eaffebb5a582047a51e4e53644247c4ad14e95dcf51070bc8748691de862db56543447a9993eb5907809884d69eeaf1026b549be04d95a7ba703a48494ebdf41d17bc31dab9d7c1a4eb9b8cb0b88cd2e55617338b8c5a58d8feb896df6dfbac66c6fbc56fcc0f17a40fb03339bdbeaa70f0c1de65480e3ddddf1ae71d64dedfd200c263962df45b20761265f747786fac48c4e890695c371113ec4175b2234fc81296ac9116c9601332b99d0206321e58b16bb0d7a69e49a26a96ee1f0b811031d79fd51dd6d0e4498cc7f65e385df4287d72ed0a4a4dbe019a545abb5ebb5f170058a18293f41ed1228286e4110fdf37e6e04aaa156bcff40c7038ed380fd8eaddc864faf7813ea3349785566d1f911b529990df437c2a4f929622ce6571923fb7087485996562785a046a44d48847dd949522a274d3fbef4bf000b9c9979d64df5d77c0efa6c5e4680e457e02379f7d9da2a35de7dce57aecdbc696d4f2cc02d802117a371bd456af945be6d3e9e2146b25c30b7da50ce4dea9055fd1c429ed8ff3b6a7d14f16fe20d8f9ec8802abd3b8f61083c1f05cf3c31369d29938ab21e1a0aefc1f5f2eef4c8ccf3cd9d89a930697c9c9e0042ac63288dc70b9bd5d86e36fee93d5e2eef36a89e3db13cd6be53c37649a0e287ad71caaa0cd08dc46b5c93952d6a9540408017688facad9f4e7c94a24f80a7e8abe7352222a51a82c80a5cf3b0d11f1b071d20b5c67f2c120b8a7f722abc21fab82893fbc206501e4a5b8fd3e881ab39b89ec931bbf3dfc2f9ffa139c9a518552ffa4b9547d88a53cc7947f6c8145c33ae6feb9bfa3d7ed81a7df09827e16033ef88d6990fa158a56750d1f554de9786bb2fa83fb593f11b7747633ac9978c6991256e5fb0ca9a7a70b26fad4d27baa338e8eb3ef2cc3321361a7b44b84b5a5093dc6a04883c5843d2125797cf4be06297cf765eab5b11a2d2c5f3ade90a787ea41b236e1c4291bd547de7c16ed4046576ba515ba1786d16a9d7e55ba802c43702e1a7f4eb842b4a0a96772ed3ccf9a8d53283d5f2a31fde784532cfe8cff41802a72a3255f6998ab70c87afe02271b5ca1ac137350f68636af6a540f0bfaccf2074b0e3b562d7aa01af153d118c2375f053e916152723a1dc3bbc16c0d8fcf9cd2e494f71540f0610e12a6f871a3848b4b96fb73d259e4108ce519dc0fb10ebf6a83aa9fb407a9578cb12ae2eebc43362358255c093c66dfc22d6a2340d292b41a6a303c116579537134ee370eeec1ba522f131d702130b5f3a49defa3941176ad0789abeee3becc738ecea0140ef69f574cf0bfa214a9a4a9bae8fc78831207cd8784c917b967b502ab4a36fa814aa55e50915ce6adf77fba178c69d94ccd5b949a7af66e74de04232563e2fdef2338b83cad860e8b29259888c35159f8e64bfa34ddd87b32450c04770f20e6db09c137fb9016382987673cd00c284cffc207b47c83f6d6e290c287d4a1567d94a012043c4a51d3403467b4a21a7c6276f7239dde0966df4c194be5951b3c2f99480ab17f11463d9625bf27656d0b8285d3dadbdd73ecddaae62c825f621bdafcc7af4370d13be788e472b2975d7d55689341f9baa6e149e46b6826ca829a349172faa749f97f3d6ecbb7af40de6b28129aed0ff96febce04bb4ee7be532bda99fbede436a027b1c665bfa7675607fb57a5372ad9152ce057aa429ddf38fa3c9bdd8fd6206c49ffa21fe8f295eba5f54ab9fa4d609075877e8502fca77deaacc884564262bb091c2673a1ec8799887adcfb4e704127113eafa4c3d9ff3e65999049c92d2807c3799be3393ab73d781f5b8e7c20ec0c06428765120f54fd38e51b4d1e1c20db2e34f516ff69206330cb7acbca9eacb1ca8150b1cdc5389e70ffdc7d51957acef211a7eaa79fd9f77386681dc5e38f49dfc57f3825574b15b15656fb0945c7a681fa170a9710061fd7caf7abaa47cae606ac96cb7f1fbe0068c2e8f921be971d95596059a93fb8deeba5d4a62cb47e8dc1dce320e84038526c7c060e9d417f76f0f73bcb76f75c645230e5a90700c828274e2014c7518d2e1ddd46aa16c747153524b5e96cdebcb298c811e54753b3f43334537d058ae0dd56e20305133cfa46a5cc7006fbbea66a8b984cc8544ea5a76f0c2ed9791bd26ea64647d4eb00661f3e95693b61f2bc7ff947076ede2bb5091c147ab4909cb5621327875ba61e68975ee5e03d45e8ddbaae869db24ff4f70adda4dbe7172e4b36c6964a0ace19a86e08fb21568608cd852cab0449596ce4b9e49db0cd51a50f2b1ca224b9c96b8c1cd106aeb48c98ef00715f28fada0f7ba49177a3d335b7d009cbd88d6efefa71054423c1ac2c035ccea176b1042ee93168ca564503be614e44cabe8e122f183ee2cd6f49ad5413a4baa3213bc12c22518c6e87dcc93894b68ae158744f9f8e6d8e85a2bd79d8ffec811994db429726802997f02547c9adc375c68b458b373e9b8f78a7929fe22894ac92778dbc2efca07d79ff57b299a85fe25de4a6d01408bfe07dfef673cee97649565b439306e36bafc9baa89916b7ddb6dee4fc1912aae243fa0aef0c2b63e1850f22049737a5f8d2dfd273da4036dcfb1e9bcca5afa48401ac3823b00b4e5b16793a859abdf9da123b1ede0a42cb2c9005550a95039882814e58f6baa6311999faab45021217ab8b95e78306b6a172ec0c8ede1d7ce86879ff48851d8f8b03d000d04532155db191b8368833e7917bcb257ffcf64f821bd82a5829000182e20381e8022034ec77824ca12b84726221d0f555f3f2259556b034481de7444a5c4aa8bf1c50d82a5828000181e2039220206228c3dd993b0b2ae7a15517e751ab3c3eccbdbf66fae8352ca91edbd307b200000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2f32441", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x5391369997f0d1335d275d9f74b26e1b6633b7949375e065d75ecf3e9ee1b478", + "transactionPosition": 35 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0717", + "to": "0xff000000000000000000000000000000002d071d", + "gas": "0x465036e", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821915ed590780855ab01ab68d7f7911622a692d9f5e153cd7fa0c876e34bdc0e919b9dad348d36a5ca7da172d7861dc9d374691ff78598f681fb630f618d5ec6dd97e4d8a4e0fdf3f470e0a270b498470e6077d62ee727c3befcea4bd75f78fd5e2baaef1154100a718f8cdf3f51606b032345be315e76db25515c551449554df8986c672cc635509291c62e177d32302c0e4786c892890efc83438b702777075fbefdf555bed7bb2247ccdbf5180e39269a95e03be6be89dc54665ef8739adbd1deb19b654a2a38cbfbe57591f81c1270883e1cf67fb9aa629166cc561ba617833a693a205500021a0cd11a4819df33dbab56d16b9c78963c2673b2c7a4ac8de6d05ef318cf273434c9a2abe2e0f052e1862d2fc1e6e61f3edb2402094f81449ce1dc7be7e320a53970a2da7e93ce09803d02b5d281d2a903e95d032a668d2b1f841f6ba1f6929e8ba6c1bc2b5529fa2ae454358fd6a8ae66ebcfae4fad3caf51d07ccac6b7eb6e0cce37e5f33b934b9f3804572d36e8890793e24685e0824732a9303cae07aa3cc5acf931554fa5a27cbad77cbd038aa052d7f480450aefd205ced4eb9061e7284158d0885a359c90ec391a13b7dcdaf1813dbfdd47dc891733aba9cac4783f3ddcdc48cb6b1921273cd611b6fad160a3fe55116777fcc891e6889bfe15dbc00a040dfd74a3d22cb5e3be517f90ff919d15788b29c50d80bf410e92c7ec237f0612f93184c4e0da45f0b200ab37719b02a6d62d04d153c2db8a667dd2dedfc82351d9337db03a47cc8725600b06867a98a469798098624a596ceedc82cd48f86a62dc8eb7f7e99826b9631a85d6fd16d82cd6d71a4688eee0423c4bb7b3edf2486896cff4d406aa42fcc63f0c43aafa8ec1861ba4d5e518d898f9952017faa76b718ae54e2a09bd55e3ddc6330d3e01496b8e16284d5af1c22adb0869974620dc3e4391b2fb94e86c25a724d51fea1fd2eaddcdb455eabfd8243e1e5433802bc68b8aa42651290ede7c143442f2dda975d26c0d4826866e1087526d18a250b297cea211cb48f13ad95b7d2d2f8828bdd07ca84124c296536e2cd5cb4dd61a1ac2f544e079c716bf706216cb708dbc5fb8230e8898b1bf17e822e198a78d7ea9d7d704bf8f92c0190075422deb51286b5f282571527ba28e18abe3ccc5bebccba440c9fb1de24b5c1104584082f2109f5e499cba9d3dae4f57c0302d38b41d10f6f0f108678011f025affb7a754c400b85cc54aace92ae21baea706d8d76580c57199f7a64cfdf087a38982597b3199af6ca2900ee6652fc62340cc1d60157e7fc31b2c474ae9938c0db0c31a3edb7ad52f086c299551fd5c60b1b6231281eda99598c5a8a9fab4c6b523c25d03415c071d9cfc252129d2fa2319cb9af8525aeff2501cfb25d443952f1f3147bfc1fda017550abb58ebeb17b9327c2c86d059cf04cb2d29966fa689d9e871c45e32f16f322655f5f5556e55bc1a4ae33d68ce15235816886fe7ce919935dd497bfb829c8fef3b22c8e5a1e0bb3e446bff9d0b458dd7531778d144dfd38de1a220e6cda4da4708930f8265d4db9a3fb8a73ae369fa128d49181a04908fc9aed5cb5c6e55caf4786d878ba601cbf32820939679af0c8a99b4fc33d5b343dc3d428258e9a802ed92d6ae7bd6b874198cfbe168738536975ed78666b6cdf8d3155990c913aa782e39d9d6feb1229b4b743e32a2ba22cbaf681945b9eaf70e3e49ec02a3caaf478c3cee83a96c775823d02976408c0633c100b27f8cca832ba01e4b8f9a50d3eb8295abdec2d1a06e4aac519e0ba2c5c16779f01c4dbf6217a55ac9125b4fa5e940fb50eff805cf6aeca0435a52fdf674808e088670c7efbd9c8cf59480dad35b7279c25e0b4a7441e169d78ed79aae7a48f29c98669cdefc6ebb71f66fcdbe1f1132adb2ae7dc3a714a2cb4a4f5865d1ec9ae4c2930511b9f9a16e220ee2afc51d056c7b69e8bcdb3a7ef8e9c264a499c6d1839f4c6161859ab00e06795b03d33a13cbe6e025a7c5ed1b3d6044b714b25ef4228841cb1326e9a198fec9eaae849f1571d7a988f832ab45d6a05106344eb0529b07b4649bdd0592e602e1b2a263ef675e2fb5250d0fc12bcc1482a9102568c9379611cf7ced996cd9a575836ead5474162a2a93c23888edde35d8fe83d4d4c1306feee83f8c39c4f9ad788e5687f67228715d99f512b40ded4a799d620d81c858dd5126a69dfac4864496cc980004071a578e9004bdce44ea1371f6acfadd0b3e1afa6a293e81eb3ab08a58958a5be5d0d5a9347e0b7968fb694d200078e991c2666bc35c2d27000427c6d9d3390ab4c1db1a92554f2891a06e196438556a18b8e861d815a8f1c1bb5d854fb8a06b983efff0ce867571d9d6d2c57315407adbd16ef8874b27f8813a3f8f8729705fce0084eaf6387ecc87a23bac38b25eabdd4067558fa08305e4a349aa2549f25217d5aa7f4246ca354196e56d51032b673f351121a217fbea67c47215c397ad20729eb402e188e616eecfbd8397f67a33c14eaa247741331d54e4d3de3e8755a09ae65f1e1b5e83779f03ade05a0076fa1272324c4ddd2caa3ef289038d11e55d5d6f8f47262d5b441c21930e5d718556d13350cb0153688ca40083ce5d8a6cfbda9f072f0a096806d16c89e721b86e7f8b0a7a26b91aef4164a2581db740dd88536c967ed71cf12f22794ad66e00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xa3a5a2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe01589cddd43f6bceff20e5ae7715922ddb41b97e88b5e4520fa355684aea943", + "transactionPosition": 36 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d071d", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3f23511", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002d071d1915ed811a045aacad5820daf497f015b8511702381fed6091635b036cdaf22a594e43cd2d97f95eb90f6b5820cdfd7ccb0815a58ba76c8db18a2801a5fdf6e4f7bffebc8a48086b5769d838f4590780855ab01ab68d7f7911622a692d9f5e153cd7fa0c876e34bdc0e919b9dad348d36a5ca7da172d7861dc9d374691ff78598f681fb630f618d5ec6dd97e4d8a4e0fdf3f470e0a270b498470e6077d62ee727c3befcea4bd75f78fd5e2baaef1154100a718f8cdf3f51606b032345be315e76db25515c551449554df8986c672cc635509291c62e177d32302c0e4786c892890efc83438b702777075fbefdf555bed7bb2247ccdbf5180e39269a95e03be6be89dc54665ef8739adbd1deb19b654a2a38cbfbe57591f81c1270883e1cf67fb9aa629166cc561ba617833a693a205500021a0cd11a4819df33dbab56d16b9c78963c2673b2c7a4ac8de6d05ef318cf273434c9a2abe2e0f052e1862d2fc1e6e61f3edb2402094f81449ce1dc7be7e320a53970a2da7e93ce09803d02b5d281d2a903e95d032a668d2b1f841f6ba1f6929e8ba6c1bc2b5529fa2ae454358fd6a8ae66ebcfae4fad3caf51d07ccac6b7eb6e0cce37e5f33b934b9f3804572d36e8890793e24685e0824732a9303cae07aa3cc5acf931554fa5a27cbad77cbd038aa052d7f480450aefd205ced4eb9061e7284158d0885a359c90ec391a13b7dcdaf1813dbfdd47dc891733aba9cac4783f3ddcdc48cb6b1921273cd611b6fad160a3fe55116777fcc891e6889bfe15dbc00a040dfd74a3d22cb5e3be517f90ff919d15788b29c50d80bf410e92c7ec237f0612f93184c4e0da45f0b200ab37719b02a6d62d04d153c2db8a667dd2dedfc82351d9337db03a47cc8725600b06867a98a469798098624a596ceedc82cd48f86a62dc8eb7f7e99826b9631a85d6fd16d82cd6d71a4688eee0423c4bb7b3edf2486896cff4d406aa42fcc63f0c43aafa8ec1861ba4d5e518d898f9952017faa76b718ae54e2a09bd55e3ddc6330d3e01496b8e16284d5af1c22adb0869974620dc3e4391b2fb94e86c25a724d51fea1fd2eaddcdb455eabfd8243e1e5433802bc68b8aa42651290ede7c143442f2dda975d26c0d4826866e1087526d18a250b297cea211cb48f13ad95b7d2d2f8828bdd07ca84124c296536e2cd5cb4dd61a1ac2f544e079c716bf706216cb708dbc5fb8230e8898b1bf17e822e198a78d7ea9d7d704bf8f92c0190075422deb51286b5f282571527ba28e18abe3ccc5bebccba440c9fb1de24b5c1104584082f2109f5e499cba9d3dae4f57c0302d38b41d10f6f0f108678011f025affb7a754c400b85cc54aace92ae21baea706d8d76580c57199f7a64cfdf087a38982597b3199af6ca2900ee6652fc62340cc1d60157e7fc31b2c474ae9938c0db0c31a3edb7ad52f086c299551fd5c60b1b6231281eda99598c5a8a9fab4c6b523c25d03415c071d9cfc252129d2fa2319cb9af8525aeff2501cfb25d443952f1f3147bfc1fda017550abb58ebeb17b9327c2c86d059cf04cb2d29966fa689d9e871c45e32f16f322655f5f5556e55bc1a4ae33d68ce15235816886fe7ce919935dd497bfb829c8fef3b22c8e5a1e0bb3e446bff9d0b458dd7531778d144dfd38de1a220e6cda4da4708930f8265d4db9a3fb8a73ae369fa128d49181a04908fc9aed5cb5c6e55caf4786d878ba601cbf32820939679af0c8a99b4fc33d5b343dc3d428258e9a802ed92d6ae7bd6b874198cfbe168738536975ed78666b6cdf8d3155990c913aa782e39d9d6feb1229b4b743e32a2ba22cbaf681945b9eaf70e3e49ec02a3caaf478c3cee83a96c775823d02976408c0633c100b27f8cca832ba01e4b8f9a50d3eb8295abdec2d1a06e4aac519e0ba2c5c16779f01c4dbf6217a55ac9125b4fa5e940fb50eff805cf6aeca0435a52fdf674808e088670c7efbd9c8cf59480dad35b7279c25e0b4a7441e169d78ed79aae7a48f29c98669cdefc6ebb71f66fcdbe1f1132adb2ae7dc3a714a2cb4a4f5865d1ec9ae4c2930511b9f9a16e220ee2afc51d056c7b69e8bcdb3a7ef8e9c264a499c6d1839f4c6161859ab00e06795b03d33a13cbe6e025a7c5ed1b3d6044b714b25ef4228841cb1326e9a198fec9eaae849f1571d7a988f832ab45d6a05106344eb0529b07b4649bdd0592e602e1b2a263ef675e2fb5250d0fc12bcc1482a9102568c9379611cf7ced996cd9a575836ead5474162a2a93c23888edde35d8fe83d4d4c1306feee83f8c39c4f9ad788e5687f67228715d99f512b40ded4a799d620d81c858dd5126a69dfac4864496cc980004071a578e9004bdce44ea1371f6acfadd0b3e1afa6a293e81eb3ab08a58958a5be5d0d5a9347e0b7968fb694d200078e991c2666bc35c2d27000427c6d9d3390ab4c1db1a92554f2891a06e196438556a18b8e861d815a8f1c1bb5d854fb8a06b983efff0ce867571d9d6d2c57315407adbd16ef8874b27f8813a3f8f8729705fce0084eaf6387ecc87a23bac38b25eabdd4067558fa08305e4a349aa2549f25217d5aa7f4246ca354196e56d51032b673f351121a217fbea67c47215c397ad20729eb402e188e616eecfbd8397f67a33c14eaa247741331d54e4d3de3e8755a09ae65f1e1b5e83779f03ade05a0076fa1272324c4ddd2caa3ef289038d11e55d5d6f8f47262d5b441c21930e5d718556d13350cb0153688ca40083ce5d8a6cfbda9f072f0a096806d16c89e721b86e7f8b0a7a26b91aef4164a2581db740dd88536c967ed71cf12f22794ad66ed82a5829000182e20381e8022007bba645c0115a8f7d6f67c1f9d2460f73d641ceb84997bb54afe36ed11a9c72d82a5828000181e203922020756cc556bd2c6db9ffad0a86b29297860d63869337c1d5fd0921e71afa592a29000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2f75775", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe01589cddd43f6bceff20e5ae7715922ddb41b97e88b5e4520fa355684aea943", + "transactionPosition": 36 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x3603c9c", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b685168282044082054081820d590180b9e0650e2791bd2e265cf4ff157de041c454b9813e09d21651b6b132609604b77b8a54feec5a9596f79a7aaa2e96fb50ade24f30190a0cb5fb7fa0d77780812aff6c95f4d15990f648a57fc4b3af6ca33ce17b1b0ba17c59198975f7dc79c231180918651145f631f00234c7697c8ad40d293cb34edd09af17f4f2a1f950aed915d6e16a18540542154198516872226599ef6d287379e87c82cfcc40bb6f1c4226d1876235d0e4c2cf7e5c830e7ecc81df37f42fbf714e393a501b80496549d1b52ce5ca02ba411c32ae4b89878363c6434ef47dc39a36027e01f3ad4c60427cd9e9d65595421a557624fb76b236702e94d4400f3bfe6814a14a0fdf34e98ccce46c551a77eea8722ff0b61baa3de7566e17d752e01d4c6ba769be27e9d4dbbf0364002ac4a50774c06353273215ba88c4a820cb9e4a2e70e7f0f4f12f95977b66fba1bc3d5a9a1549514fc23d4d39d9a6233d894761862d0763d7602eb764f05818d872e2ed5c10f0b9dac5e2540c825b6beef6049f00056ee5eaa72bca70be1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000" + }, + "result": { + "gasUsed": "0x2c387bc", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf62bf0708f9536497bc516be3b6d547abc74b04e46598b8d428d1bb989175294", + "transactionPosition": 37 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3964", + "to": "0xff000000000000000000000000000000001db56f", + "gas": "0x5d6ade4", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000010185181a8182004e40587e20e402c98ae04030a2159e81820d58c0810a1d1d92f527d5c666cacddb7c8da0c4319d0b04c56b232ffaf060c49cba4ca620bb1159347958d2681a5727efd7e090ed851ae5a9236758cd90dbacd940cc7ee8e431727a501fdf14809cb9d343520a11b67feb31f49da7cf1401fff4a04c10fb9d02be4807a8c83a812bf690ea6a3e7bf377884f8ac908c8e02f4d10bd7adc51480159b52ca97c5e9b9d3ee9c4e98d2b48aba3ea7de3710ef8f20a85aa25feff154a5d62baa62f446ddeebb8ca723c9df71e7b8f4021c48871459b65cc721a0037e5f75820a3a81d9f7df6ebf387d44874769074545c6eaafa2e7d3e3728a8b70a048a411800000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x35c5604", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x36071df471f65c45aa13469b5e27b464ca5467f7a0c32a24b7e35e02ea17953e", + "transactionPosition": 38 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001db56f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x28f29a3", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f8246013800000000460138000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x15c9767", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x36071df471f65c45aa13469b5e27b464ca5467f7a0c32a24b7e35e02ea17953e", + "transactionPosition": 38 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce379", + "to": "0xff000000000000000000000000000000002ce3c0", + "gas": "0x326ffc0", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000708181870819436cd82a5829000182e20381e8022028d8fa5716e16415633900cf944c3a7c13c89b4b7998357f96b93e52033ab7101a0037e107811a0454802e1a00480c92d82a5828000181e20392202026901d7150173a8f624326ea93f6aa265f7d4bea28071817876b420b6bb1763d00000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2270b3e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa105b44c0b659b3818b548210b3ac8c78f9d1320c94d359df1a499b8e92b1d69", + "transactionPosition": 39 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3c0", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x31b9999", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa105b44c0b659b3818b548210b3ac8c78f9d1320c94d359df1a499b8e92b1d69", + "transactionPosition": 39 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3c0", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x30ad451", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa105b44c0b659b3818b548210b3ac8c78f9d1320c94d359df1a499b8e92b1d69", + "transactionPosition": 39 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3c0", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2f8bee0", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a00480c92811a0454802e0000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x488c45", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e20392202026901d7150173a8f624326ea93f6aa265f7d4bea28071817876b420b6bb1763d000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa105b44c0b659b3818b548210b3ac8c78f9d1320c94d359df1a499b8e92b1d69", + "transactionPosition": 39 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001614ac", + "to": "0xff00000000000000000000000000000000018a9d", + "gas": "0x290f9d0", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285028182024081820d58c0a59157072c783a23fdad72543a912757d0234de28719fa60979b8cfbf9cf4eb5f7819b9e24f2aad13efb0e64f69f20b0b1fb96008c44c1ee0695eb0740aee9ba1106fb832481216b53b6929e9da8f3ece77b6685bedd0ac314a12dc9838f7b4612c874b708ca9d190d31d22f5c362625a6993f9055ca9078f040aedbd1195823a13b92e4f83234cdbd69f7df84741fd89704df8fb5d8455f4c1590aa377ebf35ea6918e47951d1ad4c22f64cec35a81bda8e2063148a98259e3689b711747ff81a0037e5f6582080334418ee2e59dadfdb8d33bf996c9c35f36dbe740e214e308a34ebd796ddc90000000000000000000000000000" + }, + "result": { + "gasUsed": "0x21a9811", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x5baf436ea76a5a6938726489a2b0ac90d8a62225ee0a67bf9440d9b66c8ab007", + "transactionPosition": 40 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c3bf7", + "to": "0xff000000000000000000000000000000001c3c0e", + "gas": "0x6a72102", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000027a8518258382004082014082024081820d590240915b93366d1917d73ed68ec773c15b05dcfabe838b6aef263760ca3e6db2c6173be57b35130f6fbe36edc2f923b97f74a48c1b20703f76fece172c2f10ee57dcbc723f46b02d971a1407358f025066b292140a32797174b57406603f80bb85f70ab3669c9a27f96f03808f47de7fe08a12149ca62ee727ebe4de79e3f7e547781fc5b6a86d3974c842f134f388418f39b7f528104398df97d94c20d49793e55105399d0e591cab05e828f98ef04f48796e367696e4ecd7c9cf48fd989199905d891f08abd8c6ab58bae7d76cb0a142e875e259f7cfeca88ca0c1cc42798c516abfefd08366536984294f0463ddcb287bac2b2305440a2d11b80bcde36942dbc0a900e8a8edd3f74bd8d63479a011a9cbffbe20eae183469f8a093bcec950ff0a0dffe0a0d2039dd24fbdef242e0d7a81e5d375443ca995272da2733c8f1087552a2572aad7049c15d007e59a5f19befab15b2c01a196d4c131ebf118c4beb33bf1ae866b5f1d89906de32cad27e4c9c9e824108d07fee9d5938c62214a10fbfcad0efef90ae507b4b9b8f20786dfe26ebdb215adda0d3530bef4619c99011340a3f6e294391e5a70cd8f3507e046bf54a27b703a49354359363d301723d6eceb1760bf220ecd15753058e87d3b489fdcf6bdf391a6ceb5c932e9cf9e3e83fe53087f8c9ba3f369567dbe5d597c133534dc3d5961f71fa1cca16614e2871a102aa59af9bc65a9f69e6babb703e41fa3cdb2599140ebd81769ae17febc5700d59e556be7bcc6ab0f245dac5cab7d98f0e0565d1b4d91505ff1f147f1a91aa80d9d1a0037e5de58201bc00903e90ca71cf8a383db0b4d3a038e6c65f02639a026af83a66ff71862a5000000000000" + }, + "result": { + "gasUsed": "0x565a543", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd88734b87797aaa8445444670a2d6bdd3bbd84d4ec90d3b7b78acae141f7c2a1", + "transactionPosition": 41 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce375", + "to": "0xff000000000000000000000000000000002ce3b6", + "gas": "0x538030d", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782195b765907808776753106795c7ae26573ae19505c7fc8a145840257c67736fc00ed90d79d770f0bcbb1d914234ffc6f522a4aba7ae8893e8fb235797e8c7258a8fcbfedf14a90fe28bbfa9834fdc4e5030caaf9ef6313a756804a9c01e2a4ef6cbee65d16281897d1285160d0504c5e42d119880b6c1967694b390b219079086f2ebb8012e17f3866f16f7be2224bceb0327cf80150ae72b36802a974b70541a9613da97ff18342f052a8a505b89b83f948fc6ae6e467bfdb19b5c9ef2c392bd54052bb1f13877b77cb9ad7cdc31bdfbd2ea960a82b6196e6c6d4fe8dddc50fe40c6f2bb4ceb0ebb1802548bf4f1f16ad3890285c00b89aa1d36745ab91d5ef87d043f15a1d8d1dc392832c578b40f456e6e1e4adbf21363cf9f0905ef9f32a1cb10ba0ac9010acdde70778af71b6054348355503a303951d23be9f4c559390da5e1708098e34f37bb0dcd3033549e902c4c1f3d747b2212f19d3c0f10a10e7176022832c521afc8f6b58407de713dc3cc35940762cfabf4ae2cb11b6d730529f4f37dcf94faac4bf6007443e3d7fa2fc3ed9207926345d2135759822096874eed1156a4040244ca30c55e64954531d1e0ac583b6128ec62a2d5adc76dca441a36884bafc29937bd304187e2c75f3ffc04b522a86e381fe672dc5299f9e53632f01d52c78a81495daee861015440697d3d49cfb4ec14c4371b5931e550168703a4e0cb329cfbf0bfccddb6d4a686b961a5910176a3ab16e4b57edfd8dfc7217bd51836bc9fba749534a635d2ce58b50563087e3a59a0a7e0cc897bfe3444792f04a3f52e375a4c7de5cf5eb1077b9aa09fd2ad2adec73036686e29548174b6515e0845d5c5a32635c25253ee3a4afb699f00ab6c720a3f82349679c5ed59ab01a70c68e4ddf395cc14cb70ed8a1645b9007226c4b733e6136d8493430420a155caaaef3e9f40869873f0d1816baa43388948f8ce129405a8ff08d514ebfd190c519871ee66af8f599911f6f10d0b727a09ad16621d1b804c32e6d906152e8651e754d58dcd2ec572696175f743b426bf5f9bd96996f53083f871fa9628eb93fe223438e39448bf72ad9facc6c2649e29345f3aaa6a26140bed5c6f524137e9789ed17efde3b814e2b16eafe8785fe83fa7d66e53c76932f20cf89053e4651060552134686a70a8faa289a0baab5d8bdf5903621347c6b9d30bf6d6d1c227148cbd69da134d20baf7597b0314737a8c30a00e13362f12a7d49b2b8c673346ed47d70e0c11423cc97f7b62af88afccc4afeaeed0f8059b726785b7e459c4a620145ab96b6ee7d683420761b1f5a96757676fc33b528d5182f7b8e84fb84ab36eba1f39db0d551b59af3258e48317d70a612e79afa2302cdfbc89d99905aa6026d24cdee78ea2d5f3145e9cad4531b2550472914cc5ff89780669bf5b7b90db9f518801c3fc317721469dcdb5ab2aa22df8a55f3db7e7f62119252fda66bb5d1201088b68faccc1403ae074c6f8085f436eacedd09a9286396ad0779ac01c50dfb60ded4892fde0ec55c56b702ec5ec3c886f9ff62e84b88f0252460118d411ca853216ee009b948b07a60cd3f4689135bc857663fe4ed71309e7babe6d0750f15e9225045b506a8dd78ba11138d131c30e7175c9bcf461c1ae51cb232d1ee4361757d729d0cea9676f1e6de815fb305e390c0cc402f1481fffbd8bcf49ae6ab8965941bcad082a712c97975217f189c434cbadd60bce79381f9f1b42de417d9e0ab162027f4c00c09bf0d01d84fbc28a74079d94462bead8c877a09bf978b8e8ec49816123cc244613c0fe60426977348bbe2d75b18f59840e4cba2688992e5713493514c6f2ebbecd2c3650eed7c3018614bec7030211b9638bf78d1b74f2adc51d3c588eb05a58f6faeab355e620c0091ad67fa3b5a861ec0fb700c637680fff5468e99bea0257f74e38ba8fbcdd08ddea960403db1af19ba1df53e371b0ae6af65fc3996effe4e8451b49fdc81c873ba134d06d38ccfc19a589d135ac1501cf693fdcc897b15b044736a99a47f8f69a48d8f58297ab20bb43f986209c0c3d8862b055c66611d040c50a281bb349c82884f39f3d9258febfcfcd6b359a03dcc0455ee5e9fe97e4c89350871a21cb0564fb0ecfb45a3263f10fb0d74bcf8c5920dd677472c45a519f63ef0b1c71344d6f2f4a95c2be6a344b20a8470a0b4eb3b2aea6a1c94c9ebc0fa8ac142432f81361b87e2d73e768b6d636fedf68e02a0481173987dd0f3d0e6707cb517c8e9364bfe2b24521459e1e9ab2fa00932c0ea67ab512532b3870e6c7f5b885911ad4d96308568461d5386d370d9a081573585105bcff4b83867b466b8248a626d41c8dc0e88345145ae881c0a03b9a157e002e822ec3b6ae93e1e35dc7e40a5e9f4f7b828eded0a715ae79d61941cff6bd265efd6cedac3c5db83c50a59ce24ac3779a5055627bc52d2ff33b335cc0e47d517e06f3861bb08d10135516097ec0cb573f3299b58e37cf3a431627b8ddcf3b0e894cab48bc3d16ac21b17ad6488977a2991c31936b6ae1d83b2988de15eb4996850978dd2853d9608b4d7d90e6d3b6198fbd064403012f7918829b806669194b55eead50487d9a89ab7c3e808cb6b6f04613df480eea6bb837b8e94790d5af979fad39284d609c1fc40bcf347466bc24434f617465178d7f89998da9da550cc49d54b18807f6eff00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x6badd2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa96e51f481ad7fa03bf8bb21077c475f6104053a8d553f7be0c7ec6bdbc5f4ac", + "transactionPosition": 42 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3b6", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4fd333a", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002ce3b6195b76811a045356e5582091f30bc594d5d33c5fdead7467a5c9a5f5d3493a04d21054be0fd86e552edb33582059f127e49b8051719946f330b970a0c841fa53270c5b6e5453ddfa01e9db20155907808776753106795c7ae26573ae19505c7fc8a145840257c67736fc00ed90d79d770f0bcbb1d914234ffc6f522a4aba7ae8893e8fb235797e8c7258a8fcbfedf14a90fe28bbfa9834fdc4e5030caaf9ef6313a756804a9c01e2a4ef6cbee65d16281897d1285160d0504c5e42d119880b6c1967694b390b219079086f2ebb8012e17f3866f16f7be2224bceb0327cf80150ae72b36802a974b70541a9613da97ff18342f052a8a505b89b83f948fc6ae6e467bfdb19b5c9ef2c392bd54052bb1f13877b77cb9ad7cdc31bdfbd2ea960a82b6196e6c6d4fe8dddc50fe40c6f2bb4ceb0ebb1802548bf4f1f16ad3890285c00b89aa1d36745ab91d5ef87d043f15a1d8d1dc392832c578b40f456e6e1e4adbf21363cf9f0905ef9f32a1cb10ba0ac9010acdde70778af71b6054348355503a303951d23be9f4c559390da5e1708098e34f37bb0dcd3033549e902c4c1f3d747b2212f19d3c0f10a10e7176022832c521afc8f6b58407de713dc3cc35940762cfabf4ae2cb11b6d730529f4f37dcf94faac4bf6007443e3d7fa2fc3ed9207926345d2135759822096874eed1156a4040244ca30c55e64954531d1e0ac583b6128ec62a2d5adc76dca441a36884bafc29937bd304187e2c75f3ffc04b522a86e381fe672dc5299f9e53632f01d52c78a81495daee861015440697d3d49cfb4ec14c4371b5931e550168703a4e0cb329cfbf0bfccddb6d4a686b961a5910176a3ab16e4b57edfd8dfc7217bd51836bc9fba749534a635d2ce58b50563087e3a59a0a7e0cc897bfe3444792f04a3f52e375a4c7de5cf5eb1077b9aa09fd2ad2adec73036686e29548174b6515e0845d5c5a32635c25253ee3a4afb699f00ab6c720a3f82349679c5ed59ab01a70c68e4ddf395cc14cb70ed8a1645b9007226c4b733e6136d8493430420a155caaaef3e9f40869873f0d1816baa43388948f8ce129405a8ff08d514ebfd190c519871ee66af8f599911f6f10d0b727a09ad16621d1b804c32e6d906152e8651e754d58dcd2ec572696175f743b426bf5f9bd96996f53083f871fa9628eb93fe223438e39448bf72ad9facc6c2649e29345f3aaa6a26140bed5c6f524137e9789ed17efde3b814e2b16eafe8785fe83fa7d66e53c76932f20cf89053e4651060552134686a70a8faa289a0baab5d8bdf5903621347c6b9d30bf6d6d1c227148cbd69da134d20baf7597b0314737a8c30a00e13362f12a7d49b2b8c673346ed47d70e0c11423cc97f7b62af88afccc4afeaeed0f8059b726785b7e459c4a620145ab96b6ee7d683420761b1f5a96757676fc33b528d5182f7b8e84fb84ab36eba1f39db0d551b59af3258e48317d70a612e79afa2302cdfbc89d99905aa6026d24cdee78ea2d5f3145e9cad4531b2550472914cc5ff89780669bf5b7b90db9f518801c3fc317721469dcdb5ab2aa22df8a55f3db7e7f62119252fda66bb5d1201088b68faccc1403ae074c6f8085f436eacedd09a9286396ad0779ac01c50dfb60ded4892fde0ec55c56b702ec5ec3c886f9ff62e84b88f0252460118d411ca853216ee009b948b07a60cd3f4689135bc857663fe4ed71309e7babe6d0750f15e9225045b506a8dd78ba11138d131c30e7175c9bcf461c1ae51cb232d1ee4361757d729d0cea9676f1e6de815fb305e390c0cc402f1481fffbd8bcf49ae6ab8965941bcad082a712c97975217f189c434cbadd60bce79381f9f1b42de417d9e0ab162027f4c00c09bf0d01d84fbc28a74079d94462bead8c877a09bf978b8e8ec49816123cc244613c0fe60426977348bbe2d75b18f59840e4cba2688992e5713493514c6f2ebbecd2c3650eed7c3018614bec7030211b9638bf78d1b74f2adc51d3c588eb05a58f6faeab355e620c0091ad67fa3b5a861ec0fb700c637680fff5468e99bea0257f74e38ba8fbcdd08ddea960403db1af19ba1df53e371b0ae6af65fc3996effe4e8451b49fdc81c873ba134d06d38ccfc19a589d135ac1501cf693fdcc897b15b044736a99a47f8f69a48d8f58297ab20bb43f986209c0c3d8862b055c66611d040c50a281bb349c82884f39f3d9258febfcfcd6b359a03dcc0455ee5e9fe97e4c89350871a21cb0564fb0ecfb45a3263f10fb0d74bcf8c5920dd677472c45a519f63ef0b1c71344d6f2f4a95c2be6a344b20a8470a0b4eb3b2aea6a1c94c9ebc0fa8ac142432f81361b87e2d73e768b6d636fedf68e02a0481173987dd0f3d0e6707cb517c8e9364bfe2b24521459e1e9ab2fa00932c0ea67ab512532b3870e6c7f5b885911ad4d96308568461d5386d370d9a081573585105bcff4b83867b466b8248a626d41c8dc0e88345145ae881c0a03b9a157e002e822ec3b6ae93e1e35dc7e40a5e9f4f7b828eded0a715ae79d61941cff6bd265efd6cedac3c5db83c50a59ce24ac3779a5055627bc52d2ff33b335cc0e47d517e06f3861bb08d10135516097ec0cb573f3299b58e37cf3a431627b8ddcf3b0e894cab48bc3d16ac21b17ad6488977a2991c31936b6ae1d83b2988de15eb4996850978dd2853d9608b4d7d90e6d3b6198fbd064403012f7918829b806669194b55eead50487d9a89ab7c3e808cb6b6f04613df480eea6bb837b8e94790d5af979fad39284d609c1fc40bcf347466bc24434f617465178d7f89998da9da550cc49d54b18807f6effd82a5829000182e20381e80220653bb701762a54aec57374c36979ae603e219bc00ef3be63554bf4a537c9be5fd82a5828000181e203922020dedeefe8f2842d894b8ef09f16bdb01583ef59df9249aeec829366d436be382e000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x36c73ec", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa96e51f481ad7fa03bf8bb21077c475f6104053a8d553f7be0c7ec6bdbc5f4ac", + "transactionPosition": 42 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce375", + "to": "0xff000000000000000000000000000000002ce3b6", + "gas": "0x5b8f940", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782195b65590780b0d5b0ae31a98ddcc416abef382cebb9497b1f392ad01eea0da893b1830817c4363a3d8553f7f49433d65d2a2b36460ab3e02973695e6a0dcf8a7226c2de837a3c7780f2cba244e1eca00be2d1e4aaa1be39b9bb68dbaf151ee90bc7f5a499310f690858ed287ee53d27ce21a485938f847db28229e6c4c078d5a0f8dea6bc7303c891bd668e6208356efa1c527805399282f916b8ba3082034aa4b89be8eff9007efa934a7302aea6d85c2008276e7fd39ead1fe418e5671db68e328182462baa5f085e99ea93a2081f63683629c8451beb4a55fde9204d19dcfcda05b9024bfbe4c99cf42bbf21a61ab5a89a431256a122169cf3ea980326ccd701dbb37a04c6d97a850e0d7da22b08a7e4f6ccc717ba53b5950b95abdf294ffbccada1068611c9cf16516df5df034b397b8f55c3411024b16cf1f6a83ff6e1ba079c2aa300dc232c96d2bca93e3c75bf0db07e6bb8886c95035566b4c35e8ffb0730ff4a3f42d38dde63aaa711cced92c376d56f2d848c29b0a979a83acf3c46bb5d123c6cac402b87a4c24d59310f44af1bcc61316c8d778a2da26a6e0cc3c1f0e3053431dcc3bb29021e11ce9bd74bf44676734c8ab00ac0156ae09e3ab82f82f2ca467d3d5ac6153dd388faa6ea8384692559f65ed9ef1948e400c8908f335d494aa8f611fd52acdb72c913f7b0ab498b5dcf487e666499c3507b4a418934b4c26c60da8d2d53b4d17a1497db33596126ff1bc2ab4fccbba06ce6c2b83027d82ef31a7f796464d996d4085bf7da53a2e260c8dca9024f28ae70cce80a23482f4aa19aff838a77344229c7b6884ca2d319e6bfe987e8c025c1c71d892d480c7a55bb46e5c0498fcea2e7f5d06f5ad4caee57fd3da417ee1cdeb660a00519fd939feedf6eef6a2ae2cf57ec6a588dd0415debd6edf03ff259b70b7217c394e0c0b1504eb819c1731560cc7011367e650862035c1eff36d61358aef1cd66bfb345c5c349d986afa0bc31ce62f53d7746b5bbdb027db28352f745320768b01619a152b9f766689c7573bd1770fe7704faa8eff9b8c83c064681e7bf2b345d9def6b7d57a219841a2ed3b613adb9c13ac6ec9595f59691ed05db2274062868c42451cb669e048b3cb9a5929448822ada2029ffa2a42ca2a7b97e7e1cf652314ef151242d982d0204ab49bb8900d45fe57e07484cb008454aa8cf1bbef3b797c6c8bb100c7d7b000f3c347bd0c6ee5f225ab5c579c24ca23d4358f5349423c93e9c6f9056d3faf80241b9c0a3c8b77f18bd364f400a08a50c55cc989646d1de3b8b0c2b243f4529b72c0d1d5cb316b2068588848aa784c2c1b047b63d30507e9cd58cbcd0034390e5ec61ff2990b0b07867011387e9aad8e6b38c236bb93bd7eefd961d696bd64169784ad55e9c5196a0792e070a33dd95fee21939f59b7fd4a7bdb0c3c8c56135e7f8e621ba578e59eb328426864527394bfd080e8e2c06e317693bfb7f9c970dbeddcb5e6ea07f44c1cc364a0d864a645fab0422c0e27363dbc7256ad44eb265b00c671a7c0f20b0bf8904b008bc2ea07490db2fd954cfbe48d137963c1e286d79a8e60f1285003f476ab16b5b28a79ebfa78fd689c276203f4aea3b3f43f9999c81e03a13e9c8faede90f991440dd39a34fe4763db1652a4b7dc274412ee9b836f8aab5c7e02328a2897f8f331a378324e1d2a84cf998435fd6ad631f16b1ff3566def97b6b319c95038d95135c8f1edddd7ceee4083cd9fbfd48539db28007ae00c350bae09355a3d3cd68bc7c91ec4616dff3dd300e8da3861c78cd67b58a49bb4a5c98c4d29b72f3e0fe32193e9047ff4f62b207623f0da9eb315415bb153347b4d7e7989107a2fe1f2c25572e6e7d757b09c5e382d046e5a72934d378ac9298db613281a15611b522725f058106276dd70689b65ccb27b0fbccda99c7b502af389fcc4c441738462efd87439cafb0aab34b7f33d0f44da59b90ea03d7d8e7b7a96b8b78f34ba126936a08a5c915ea8c679aadf08a72b1ac79738ca72f157cadc8246b3332d20bf1c4319c4d0b4a0fc643710084c05a44ee3c6d6316f26b3d5c05afde5b660d6534dc0bb51647a9b980dca0aa2b9430ff689dd3b9fa871d5cf594df3b25776cf0bbfd9a4644bb53c083c7b3845064ae1046b0420475b0a5cd3d316ff15ed581d62456d117021a52e8adacda6ab8365c94578a84139adcdabe3d32694258b687202452117ba61bacab047517a323b2f36607052fa48ffd55dd3ef4a78c65a1441b4f628666b623f76ef4576136ac1ef16d6f98bbaeb14d1428790969175eee0968e9c76aaedacdb1b9d77f898e5aa8bfbf4d053a7cba468090a749f80ed668cb7f4dc7af33af98935de85ea462451634378eebb58acb78f6a1021a4b023ce212d83d60402f28dde712722f72d6438142148231e69e55a1ab5fa192f5a2e068a40d60327422e302b8a560b66f8eb0eb0a94c36bbaba86f38310bc2e8a8c06d9ab87c8af8edb971581de9ac84b8c7371560c9d4471fec715f622b92bc0439c1a3b2ff4a50e2e5db260beda585142eb60851c9b31e0e4254d010fecbf7af7ce9921e5dad628acb23f5406a2f3e9b647944de96e8d6b2e91aadbdf8cd27af2a8800af5f450015eaf88b47a858b5123f718c6378c9c085da31519fc41dfcaca195a0b2de33dfd216dac4b348e4cb4b2811853cdfab8e11ed55a00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x64fb6e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8de36a2a721ac62d59185fc78daabfe4f045335c963be30edd33170cc7d2ca47", + "transactionPosition": 43 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3b6", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x584d9b1", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002ce3b6195b65811a045353365820e8191611dfc1e40b36fb948cae0d899ee686b3cd8900eab1e5d4c9f8502a022a582047a51e4e53644247c4ad14e95dcf51070bc8748691de862db56543447a9993eb590780b0d5b0ae31a98ddcc416abef382cebb9497b1f392ad01eea0da893b1830817c4363a3d8553f7f49433d65d2a2b36460ab3e02973695e6a0dcf8a7226c2de837a3c7780f2cba244e1eca00be2d1e4aaa1be39b9bb68dbaf151ee90bc7f5a499310f690858ed287ee53d27ce21a485938f847db28229e6c4c078d5a0f8dea6bc7303c891bd668e6208356efa1c527805399282f916b8ba3082034aa4b89be8eff9007efa934a7302aea6d85c2008276e7fd39ead1fe418e5671db68e328182462baa5f085e99ea93a2081f63683629c8451beb4a55fde9204d19dcfcda05b9024bfbe4c99cf42bbf21a61ab5a89a431256a122169cf3ea980326ccd701dbb37a04c6d97a850e0d7da22b08a7e4f6ccc717ba53b5950b95abdf294ffbccada1068611c9cf16516df5df034b397b8f55c3411024b16cf1f6a83ff6e1ba079c2aa300dc232c96d2bca93e3c75bf0db07e6bb8886c95035566b4c35e8ffb0730ff4a3f42d38dde63aaa711cced92c376d56f2d848c29b0a979a83acf3c46bb5d123c6cac402b87a4c24d59310f44af1bcc61316c8d778a2da26a6e0cc3c1f0e3053431dcc3bb29021e11ce9bd74bf44676734c8ab00ac0156ae09e3ab82f82f2ca467d3d5ac6153dd388faa6ea8384692559f65ed9ef1948e400c8908f335d494aa8f611fd52acdb72c913f7b0ab498b5dcf487e666499c3507b4a418934b4c26c60da8d2d53b4d17a1497db33596126ff1bc2ab4fccbba06ce6c2b83027d82ef31a7f796464d996d4085bf7da53a2e260c8dca9024f28ae70cce80a23482f4aa19aff838a77344229c7b6884ca2d319e6bfe987e8c025c1c71d892d480c7a55bb46e5c0498fcea2e7f5d06f5ad4caee57fd3da417ee1cdeb660a00519fd939feedf6eef6a2ae2cf57ec6a588dd0415debd6edf03ff259b70b7217c394e0c0b1504eb819c1731560cc7011367e650862035c1eff36d61358aef1cd66bfb345c5c349d986afa0bc31ce62f53d7746b5bbdb027db28352f745320768b01619a152b9f766689c7573bd1770fe7704faa8eff9b8c83c064681e7bf2b345d9def6b7d57a219841a2ed3b613adb9c13ac6ec9595f59691ed05db2274062868c42451cb669e048b3cb9a5929448822ada2029ffa2a42ca2a7b97e7e1cf652314ef151242d982d0204ab49bb8900d45fe57e07484cb008454aa8cf1bbef3b797c6c8bb100c7d7b000f3c347bd0c6ee5f225ab5c579c24ca23d4358f5349423c93e9c6f9056d3faf80241b9c0a3c8b77f18bd364f400a08a50c55cc989646d1de3b8b0c2b243f4529b72c0d1d5cb316b2068588848aa784c2c1b047b63d30507e9cd58cbcd0034390e5ec61ff2990b0b07867011387e9aad8e6b38c236bb93bd7eefd961d696bd64169784ad55e9c5196a0792e070a33dd95fee21939f59b7fd4a7bdb0c3c8c56135e7f8e621ba578e59eb328426864527394bfd080e8e2c06e317693bfb7f9c970dbeddcb5e6ea07f44c1cc364a0d864a645fab0422c0e27363dbc7256ad44eb265b00c671a7c0f20b0bf8904b008bc2ea07490db2fd954cfbe48d137963c1e286d79a8e60f1285003f476ab16b5b28a79ebfa78fd689c276203f4aea3b3f43f9999c81e03a13e9c8faede90f991440dd39a34fe4763db1652a4b7dc274412ee9b836f8aab5c7e02328a2897f8f331a378324e1d2a84cf998435fd6ad631f16b1ff3566def97b6b319c95038d95135c8f1edddd7ceee4083cd9fbfd48539db28007ae00c350bae09355a3d3cd68bc7c91ec4616dff3dd300e8da3861c78cd67b58a49bb4a5c98c4d29b72f3e0fe32193e9047ff4f62b207623f0da9eb315415bb153347b4d7e7989107a2fe1f2c25572e6e7d757b09c5e382d046e5a72934d378ac9298db613281a15611b522725f058106276dd70689b65ccb27b0fbccda99c7b502af389fcc4c441738462efd87439cafb0aab34b7f33d0f44da59b90ea03d7d8e7b7a96b8b78f34ba126936a08a5c915ea8c679aadf08a72b1ac79738ca72f157cadc8246b3332d20bf1c4319c4d0b4a0fc643710084c05a44ee3c6d6316f26b3d5c05afde5b660d6534dc0bb51647a9b980dca0aa2b9430ff689dd3b9fa871d5cf594df3b25776cf0bbfd9a4644bb53c083c7b3845064ae1046b0420475b0a5cd3d316ff15ed581d62456d117021a52e8adacda6ab8365c94578a84139adcdabe3d32694258b687202452117ba61bacab047517a323b2f36607052fa48ffd55dd3ef4a78c65a1441b4f628666b623f76ef4576136ac1ef16d6f98bbaeb14d1428790969175eee0968e9c76aaedacdb1b9d77f898e5aa8bfbf4d053a7cba468090a749f80ed668cb7f4dc7af33af98935de85ea462451634378eebb58acb78f6a1021a4b023ce212d83d60402f28dde712722f72d6438142148231e69e55a1ab5fa192f5a2e068a40d60327422e302b8a560b66f8eb0eb0a94c36bbaba86f38310bc2e8a8c06d9ab87c8af8edb971581de9ac84b8c7371560c9d4471fec715f622b92bc0439c1a3b2ff4a50e2e5db260beda585142eb60851c9b31e0e4254d010fecbf7af7ce9921e5dad628acb23f5406a2f3e9b647944de96e8d6b2e91aadbdf8cd27af2a8800af5f450015eaf88b47a858b5123f718c6378c9c085da31519fc41dfcaca195a0b2de33dfd216dac4b348e4cb4b2811853cdfab8e11ed55ad82a5829000182e20381e802204d0dbb2063ca111c6cf0067d447609ce7c03d44c821d8fbb2c417e96ea7b5a18d82a5828000181e203922020c5455567d46fbaa53337932443d470d07b100bed66689795774541eac6bfd92e000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3dab44b", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8de36a2a721ac62d59185fc78daabfe4f045335c963be30edd33170cc7d2ca47", + "transactionPosition": 43 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002af885", + "to": "0xff000000000000000000000000000000002afa9a", + "gas": "0x4ffdb6b", + "value": "0x1a7f287357826fcb", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a000190f1590780aa6dd1b6cde27d943f35448a588dda962b90bb5c2d83fb2fc6b05c8879362c7e817ed132379a5d26eedc593a3c606d21b1ad6dce5e114d940ed6339ad15a75f332154afdfcf7b12d95582ee4a14202d965a951d4188d7c229f08116f19110bf10ffe6f3131684f4b81a83d8f134ba8b934c9fbba9de485da6314a6e2b4506ce66faabd643095fc29368353eccc4666ccabd8aa5e770a7a983fc357e401b33944c70ad9ca78bc9cfc28c89557f36c9211906843f318dc25c061094b004bd12cba8e086520ffd326f3770543b454bc2dde31928e03115ae3ec7827456b9d1686d4221d1bbd908594ae4765699cd49f5d35b683c356421c3b3d84831c9f848ede29dd011f0b7b2b19cd4247ba549fc81ba77a935573ecb2a8e51fe4e0cbcf360a3a19cbca7006ede49b7b6520c744849e6b4daa1f79d178a9247fce67547d3b07d2e19762f3bf18dba2a6f5889aa872a843948cd7ef996705951974d2335c4b8f5162f93695a1babd67b5452e4bc9ec673031a23f3e19238e1ce953df0d6932a2b5a9a4cfdf9ec7023cb3a3532211daee7979736978bf3dfaba75b44e9890fc1e324a6ff5740bb5d5a91b57d21795a62843af5e1fdc25d90e2b087d36c3d646f526a2e9336f937d80f9e03a855a08878f80da10ea8167836870d3b8abf9ebf2b5b60f001ad41bf0e78dd27b48569c587335e59482402570c61740c114af91f1b163699498bf6b0cefd1d1d211328ce8f0cd831770938798626b188d47787e3169c103fd8acf60aafbad26c69259c269e55aef8ef27ce606e69cace5bed0e8570864881b582727c096fab89d33c0e6d2d22ac2b3d02974110bd104a36be0f1d20bf3ed4d3c4c3fbdbb4165f534eb2adf466587effe414558bd05d694c75eb071cacaf21d67d7782d4bc7b42deb129286a96d00de11b5ffbe2f07502224b99541637018c19fd37d110a3636addac50bc20bfd8c408d014e4f41c3f6caaccdeb47057447de642bfd04b7a240d526879cd484ecafb45f07e4c4728a6df049534316e76ab3b751101a606b8f7741c30508c86d68075d24bde13a68276dd9c64f2bdc20798f8d3ff9052273f3407b8a255a5de6303c2a2a2501a1f0271a3996dbad35959ce3f69169329593faea319dd23d1aaa66adb32fc8be99377176e47de9538a55b5fc6f02ed74a494dd4546953b7cc772b649ed17a4c728ecb4d63ea885293926751387f8e6f1779acbc0c8a4aebbea14b51dd4e651f8b2055c6b307c041fca08e590f50bfd1066f7e074b7f98fd4000fc8a10872fd115e4c74a21c9129fb0230ae27f4af10bdbcd02cfe96792a10628b0018ef8cf7984ab0b2fa34a9464404500c8e2d6945935a22a534962ea8ea448da54d79b8e53d56b9c2c7ad7de5b9003476d530f1bc1f7a3371a4e1dddffe93974eb03383e315632977426589e4a13022b55ad361150a48c42c35ba475848dd8a6ec957c7c1a6525830eea4ca05175c3cfb0a5c1faff0338023961101aa2cf5458b1ec14e1c958ee0a8048acac6cdde743043847c2c8951d708a00247e19a8fcc1f90325533a773ffbe795e78498ffc3d81267c2ca13eb7b9fe362ac30518537653ec47bf3bf172ec0acd9a65d9e8c16022a6ced9030033964a3505894f0621c9be0268328a4826c7596ea3f21a6f404126dcdfda7063e9e70175d012424b890043a619d74f99734052b262f7983646b7c273061c898341b4ad3a46e0c825790b4f8b43181669311936f36b7595c13f46f111c1b49bc1b18c9fc55bedd45667483a7effc18e808f5159a09530f142669318d5ad90c9c2c768351373af5cb789cd938c40735aee33285036aadc2f692dc762caeedf5f870d215c7c1b5fecd512b83dce65badc32776ee0ae289eef405ad38683de41cf605690a078d504cea2c2e7436e3b87f2bf9a89f06ca8a9a79b91cc449dabb10818c178334db4ca84d41a17d390d07ac237423365864ca394acd3a1c17dec6bea0542ef51aca5929e4ce6cc4a422d79008b060ee30734f93bfd0d2b5114f4a49c2479f2d2a2761bd7f60fa028a66c3206a393a6025cb26c546c1244b6ddb13e7bfde0de9441da50655ba2d00085ec2d9ccdb2d58068b8e55d04aa4c4283b7b8626ea6e7458cdcf6ba6100be0dda7d2eb19d2b9afc2bb6ad54a1f444e388e5e43d5160fd81ac0fd1235f8479fdc8594e65b5e45e2227348206fa42fa56fe1449825b054c1ad68f2b8420b5e669af6f79d3d9f6c36e5d1d7e9dd9a04377fc49a38e8dbc54d5a45e50a9f5504911666d08c6720967d47e27b6b680731a22100bd337afccac15368ae597a8ac55a8c548fe999c5c2c1767d04c91e547032e4fdcefde964a4901c34e6f374c0a77d1afe3a5ba1bc85a66ca17f3bb12192f0029ca4e402547b2f9b60f67925f14c7ea6f30eab4d4a75110969326dc7631ae6090e665147ff29b6b0aaa472ec0fd57937813cb55ff2dc4b47d73775022ed80e10e2aac5bbd886f1c91a09dad3ae74f3aa71db848302c08145f59d5e56a476f9ad63326fd249bb7083b4a77e5e576685ef161c65bfc3c2beb424ecf1b320c2471171cec1f8c27b04c1f685823d84a3aaf4f74b2f1a613064a75500b84f7c3635836b17380ef09f24ab3dd708af2705201af775ea0422215d6ff0a616ed0e6d31a94aad6f55cc65fd60336380f28870b93cc861d9b2ee43293063c27620892065a0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7544fb", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc0408073d9e8c991bfcb7a2e0205c700a68359ed40dcf6912fbc02d2623971ce", + "transactionPosition": 44 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002afa9a", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4bbae48", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a002afa9a1a000190f1811a045acda85820b24b3e1b897f42b136eab69c226a2ae7647adf103dc5fe05e67ef22daa0e65f8582074fea458846979d6f5d8383b77701ee74e32ded56a348a283c139f3019f64153590780aa6dd1b6cde27d943f35448a588dda962b90bb5c2d83fb2fc6b05c8879362c7e817ed132379a5d26eedc593a3c606d21b1ad6dce5e114d940ed6339ad15a75f332154afdfcf7b12d95582ee4a14202d965a951d4188d7c229f08116f19110bf10ffe6f3131684f4b81a83d8f134ba8b934c9fbba9de485da6314a6e2b4506ce66faabd643095fc29368353eccc4666ccabd8aa5e770a7a983fc357e401b33944c70ad9ca78bc9cfc28c89557f36c9211906843f318dc25c061094b004bd12cba8e086520ffd326f3770543b454bc2dde31928e03115ae3ec7827456b9d1686d4221d1bbd908594ae4765699cd49f5d35b683c356421c3b3d84831c9f848ede29dd011f0b7b2b19cd4247ba549fc81ba77a935573ecb2a8e51fe4e0cbcf360a3a19cbca7006ede49b7b6520c744849e6b4daa1f79d178a9247fce67547d3b07d2e19762f3bf18dba2a6f5889aa872a843948cd7ef996705951974d2335c4b8f5162f93695a1babd67b5452e4bc9ec673031a23f3e19238e1ce953df0d6932a2b5a9a4cfdf9ec7023cb3a3532211daee7979736978bf3dfaba75b44e9890fc1e324a6ff5740bb5d5a91b57d21795a62843af5e1fdc25d90e2b087d36c3d646f526a2e9336f937d80f9e03a855a08878f80da10ea8167836870d3b8abf9ebf2b5b60f001ad41bf0e78dd27b48569c587335e59482402570c61740c114af91f1b163699498bf6b0cefd1d1d211328ce8f0cd831770938798626b188d47787e3169c103fd8acf60aafbad26c69259c269e55aef8ef27ce606e69cace5bed0e8570864881b582727c096fab89d33c0e6d2d22ac2b3d02974110bd104a36be0f1d20bf3ed4d3c4c3fbdbb4165f534eb2adf466587effe414558bd05d694c75eb071cacaf21d67d7782d4bc7b42deb129286a96d00de11b5ffbe2f07502224b99541637018c19fd37d110a3636addac50bc20bfd8c408d014e4f41c3f6caaccdeb47057447de642bfd04b7a240d526879cd484ecafb45f07e4c4728a6df049534316e76ab3b751101a606b8f7741c30508c86d68075d24bde13a68276dd9c64f2bdc20798f8d3ff9052273f3407b8a255a5de6303c2a2a2501a1f0271a3996dbad35959ce3f69169329593faea319dd23d1aaa66adb32fc8be99377176e47de9538a55b5fc6f02ed74a494dd4546953b7cc772b649ed17a4c728ecb4d63ea885293926751387f8e6f1779acbc0c8a4aebbea14b51dd4e651f8b2055c6b307c041fca08e590f50bfd1066f7e074b7f98fd4000fc8a10872fd115e4c74a21c9129fb0230ae27f4af10bdbcd02cfe96792a10628b0018ef8cf7984ab0b2fa34a9464404500c8e2d6945935a22a534962ea8ea448da54d79b8e53d56b9c2c7ad7de5b9003476d530f1bc1f7a3371a4e1dddffe93974eb03383e315632977426589e4a13022b55ad361150a48c42c35ba475848dd8a6ec957c7c1a6525830eea4ca05175c3cfb0a5c1faff0338023961101aa2cf5458b1ec14e1c958ee0a8048acac6cdde743043847c2c8951d708a00247e19a8fcc1f90325533a773ffbe795e78498ffc3d81267c2ca13eb7b9fe362ac30518537653ec47bf3bf172ec0acd9a65d9e8c16022a6ced9030033964a3505894f0621c9be0268328a4826c7596ea3f21a6f404126dcdfda7063e9e70175d012424b890043a619d74f99734052b262f7983646b7c273061c898341b4ad3a46e0c825790b4f8b43181669311936f36b7595c13f46f111c1b49bc1b18c9fc55bedd45667483a7effc18e808f5159a09530f142669318d5ad90c9c2c768351373af5cb789cd938c40735aee33285036aadc2f692dc762caeedf5f870d215c7c1b5fecd512b83dce65badc32776ee0ae289eef405ad38683de41cf605690a078d504cea2c2e7436e3b87f2bf9a89f06ca8a9a79b91cc449dabb10818c178334db4ca84d41a17d390d07ac237423365864ca394acd3a1c17dec6bea0542ef51aca5929e4ce6cc4a422d79008b060ee30734f93bfd0d2b5114f4a49c2479f2d2a2761bd7f60fa028a66c3206a393a6025cb26c546c1244b6ddb13e7bfde0de9441da50655ba2d00085ec2d9ccdb2d58068b8e55d04aa4c4283b7b8626ea6e7458cdcf6ba6100be0dda7d2eb19d2b9afc2bb6ad54a1f444e388e5e43d5160fd81ac0fd1235f8479fdc8594e65b5e45e2227348206fa42fa56fe1449825b054c1ad68f2b8420b5e669af6f79d3d9f6c36e5d1d7e9dd9a04377fc49a38e8dbc54d5a45e50a9f5504911666d08c6720967d47e27b6b680731a22100bd337afccac15368ae597a8ac55a8c548fe999c5c2c1767d04c91e547032e4fdcefde964a4901c34e6f374c0a77d1afe3a5ba1bc85a66ca17f3bb12192f0029ca4e402547b2f9b60f67925f14c7ea6f30eab4d4a75110969326dc7631ae6090e665147ff29b6b0aaa472ec0fd57937813cb55ff2dc4b47d73775022ed80e10e2aac5bbd886f1c91a09dad3ae74f3aa71db848302c08145f59d5e56a476f9ad63326fd249bb7083b4a77e5e576685ef161c65bfc3c2beb424ecf1b320c2471171cec1f8c27b04c1f685823d84a3aaf4f74b2f1a613064a75500b84f7c3635836b17380ef09f24ab3dd708af2705201af775ea0422215d6ff0a616ed0e6d31a94aad6f55cc65fd60336380f28870b93cc861d9b2ee43293063c27620892065ad82a5829000182e20381e802206742e56094a25e9cb8ccfd548669001e885fdb05e5a3a32e414f33401d1ece41d82a5828000181e20392202072f2ba4a22ea83fd98e49f73658227ca7d4026d6c093aefc824f23e5a5acc71c00000000000000000000000000" + }, + "result": { + "gasUsed": "0x371397e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc0408073d9e8c991bfcb7a2e0205c700a68359ed40dcf6912fbc02d2623971ce", + "transactionPosition": 44 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001282b2", + "to": "0xff00000000000000000000000000000000127dd3", + "gas": "0x1d3a22b", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285078182004081820d58c0941484e4cc646ac06322666cfc99a0ed9ad905db567a1cc54211cdce6f42ee3dee60352b2be6a23aba9bba8e794ff0698928e0a8c7f0bd2ad8eadff2ce5e9750579598a700988bd54af7e4667fdd1ae926725280acf36e1ab80cc1de1601e8da010c876138e492f250edc21b8b59635fe8a8f7c7337b6c142ef29b76757b5145e1e9882e664b43e8a10ec295667d9cb2a6b2de7f8c3b15035cf8c7865767b31d6e2f2dd243b61e8abbc333640b050bb8524f7c22950fcf01306c1fdcef61e1541a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x183032f", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8643bb35c2d74917f7c4ccc8b797edc5c71e1d27e5cf731220ffe82a3533e984", + "transactionPosition": 45 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x2eafd6d", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b6851682820a40820b4081820d590180b2eb828eedbe33cad6f19452c4a64757c82c436f25cd9eaf8e04e32b77ad8322faf0b5c2faa9b927cb1f9c8e0bf7e943883c08d938f92ab436ad40321f8cea79f55f2011801a09c8728506c0d208f370617e3559354b5cbccc6e9e37493999a604e8c9020ac8f8ae7ec6318c8f907748b717f990f97a5f3bdefb656bd8e1ab058e90e1ae54d200c60da283573c6462db83164589345e177a4f12c229c60965ce0345ca9f4f375464dd4c4b22e5fe3c4b93c4a566e55f4ae5d2f888c22cbdfb3aac072a09b8d53f522a24aaef5d50a5cd405d81980c744454908cc3198eae1549d77e8f2f038dbc7600bae3709738870985f222ce07642e324042fd34bac9d89c17e327a10f5e6f1b1e15af4e85682eb5737255793ac3502890b72403550aa5440c8560e2634fc9d63ed0dac44bf3d1162666cc01adea437adc6ad2b5bf16c3e16c850c6d914054f5eff597510445ccd98600f5f6e75e683e13a942bc4c3ff3d26f7dce9ea5ad9281809f01018ea7c7a93991336e2358ad44f5094686cb6ea4441a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000" + }, + "result": { + "gasUsed": "0x27952ea", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0cfb366083cce141ec4cbcb7fbc844164a6d689001b4fa1a5233ba49ac2a60b8", + "transactionPosition": 46 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x367a5bf", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b685168282004082014081820d59018083cfb7899f78e9abf3b68fb49960e041030f4a1111d343aaf21b64aff5cb100e76bd22d19c9b785283ebfe8678bc4df1b758afeba700e80c3821f5cdcbcdcb4203a09c02283c38eed8602a4fd85cf2b7ace368e0870aae6f09269d4e2f84861b1737f0af1660fd760a0bccf250bc131510d151ee15ce9ca7949bea9f981dca6bfb9f1c1201a464982345ea31d5188afa8eea3801c8edacef9734027b0f51aeb2abb58399a43c27189a368f4baf04c151cec24704723ef65ec928f465d7673501b5e895e3968a207457fa85d74a28b0254d13d8ac435c1413fba0e64155ab4344403690d1191e9240699cd5798d27d7cd95e88ad5077892b556df1102216b13fcbd8b10a2225da004b1fd134292441bf70a721f2a846a15d6850866f59017271e016aa391968b25a1ba3ad81fd844601bfd2b57d7a40a2fc036f457dba80b4ebb2399677fe3419026d308eb9ab3e49ba08e0087e610c6f3f02f14f180549c697687f4bd3f0ea6d51ad0f238a320adc310b8a86f661a3ec01b30664a771471383d1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000" + }, + "result": { + "gasUsed": "0x2f3564d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x6e808c2937deb1c22c515ef2023ee58c101a4576db5d5e6ac159ac61ba036e6d", + "transactionPosition": 47 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x39ef4a2", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b685168282064082074081820d590180afa14917d4144b56496b6cc74319d2098d173c50b0da49bb9512bed6dd7595601cfa80897e804817c0d2478207f71d2587dd46bfcf6b13cf6521add3b7652f47e4639b19027f26aeda2f1b3722110d54cd360c30150a3bbee48dc463e3e663cd0c21b12ee7ca34be107976d7b795d7dd4112d82dc88b762fe79698d0125e512e1a44c16134cc62e91459213c2fd61bad9562f6f79e1886637d4643ffdb1c6b73d81fa1c9340db4bd820e616bb6f6b95ab0a3ef8ad4a66493ee9a309a9e0a7809a0f7cbf76c61839805069b4e4877f198f05b1635a20c99068ab282c61cc8f3bc39f7ee82a63bf4b31edee162fd63aa4491f4a1f7ca6315e8d9821e782ce077ebc413911b951bf6d428541ac0953fa6ad6b0b6ea69baf7385e9c6c3aa4a649d4d09cd4b65224f6e84399d0833ecd50cc64e2ce13f38eb38e9988d7f1191f67e85e80460abb16b217cd2d086157df72c71875a36560a72f5fa0e8bf4575d9de99122fb62eefa0dcfaaedbff79ef30a2a9d9406468359ee9481cd403de8fd3de2321a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000" + }, + "result": { + "gasUsed": "0x333798d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x3208378ba92f068351f798d2e18b7515acc2cb3706ad08828a25fa04bd8d5176", + "transactionPosition": 48 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x2df8316", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b6851682820c40820d4081820d5901808b127976370cee8e21810fef9e01d393ed9157f129f195e89183a3b3986dd47d6b1fc50411f55fa62e3e8a21b87432f5b4083668c65d107e89a712a69228bc8f9b8434cfb6cce55292911f7259ad33dc27b52ce79130347f460b6cd9af4a1fc201fa009bfdec83897255da344d1f87b4549066a6d1480d2e45d78817a48496d9170fea27163562954584ad441d094b90b70ad4eda0a538df1aa2c98ad11c53df1527b388ba6d985f184749d68c6755f589aa44158cb62b25a216a65a6d34707c8e1964f519655f563b7d8176595a9962081e4080e7cc1222eb61b8fbbef1148bea9dc54cfccf8ad8a8219e4743f42d1cb8d068aee2c128a2e7078d273ceb80b1b5d0be48e05c89220f166e09f24a92d4fc1653d899044f27801cd7be7b60516b082a25bf8e584af5d2ab269526b640e2a9b15d8bef2746851aa32ff49440a5d422503b60e4891e4406dca42d2df507f6a69222360c3a8d4974bd664bbb84996665e0e006c354d5270219b23a91b13f61b74352e5c977ef9a334ca6e8dfef7c681a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000" + }, + "result": { + "gasUsed": "0x2c63cfa", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa92fbc3e483686a903d03dad832850c63d219c087a9560d8dd11846aba05d051", + "transactionPosition": 49 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x277b6fa", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285168182084081820d58c0a00fc058f003e9ce71e5c7f0110c27df98ac84029077d1a9b2744f7e0151cf7f37003c1eecdaca3938bdbd868a115882aff1ebd7e304411ef8bae7829e72f002b092325c39e5b2e81b27daa449d1e68e653f07b8bedfe8280056265a1f69de540f5bbb84169d1bc2066279ff20e66708357d446408e3da7c4b690b0f87ab40dce7a3f32066eccc99e74612d6ece3570caef37ce3559cfdde7e2415c47ee44bcc21d4f14974bbb91207ee9bc4597be61fea4b52f2f210f58bd1ee7ca856fc56461a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x22bd49a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xac6b89882cdc6aee6b38cd3e2c2ddaa63c2d4f48c431cf8298e24c5d95af081a", + "transactionPosition": 50 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000015c8bc", + "to": "0xff00000000000000000000000000000000018a9c", + "gas": "0x279471a", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285168182094081820d58c0a16986848d181bf1de1caebee43c1eccd53a589850e2a21b63e3fd4c7157be80c013abb17ffeb957d5064bb729f24be8a8a1f9bc526100fde11a14cb850d2773204d90747c7eeec3943bb2ab043b1c7427245198329a88920423807b1d378a1d05ecc5761e6fe291dc6f1a826f8af0d213406ffa54176aa3954fbdc6875bf14dab7c3b3e37973d09e7c2501bd3b8a3dc8a9f1a14e911342e61f958afb3765d537f4442254d53cf384e210b8f22e897d2fb2da104c1cb7167f1b1dcddc216105b1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2381302", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x66969ead995791added4e5ea61637c9f338e94808ab77f703ff227e63cd158a9", + "transactionPosition": 51 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000000c4d75", + "to": "0xff000000000000000000000000000000001536f3", + "gas": "0x210e371", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b78518228282004082014081820d590180ab8892f0b4c2c9054daf7a9ed12d1b5a3a216bf4c540c2d12ff2f19b40e14a2aaee5a22012b97888bf5e150a69e6561f850dbdc6a8bce7a292800a139bddff40a363999f600294b6b7490984d83af7f92bb98c27df4490661b5bfb662ace46f20c62a9fc0db2401dc84f0548f8cb6c2bb8d57ec3d520d49aea2dc2a6416dff3072b2b901948d6474c794484e2e0fc90b95856412e33b02c965f56065eaf18a042545cf1094f16c16f0a70f671d11276a73633fd4aa4e0ff4d261d07298950c25b3b9dd698ea90ef14758e2d760a8339b6e893c97bb5a0797c9b0a199bb6dc7fd82dcdb58c3f4e8e7b525823cc28430d280d85fd1c4cf59019d8387b0edaf3c136d04bdceba76e8942efb9ec1f642d2db2f61e50e5c6917ef2485e272d4ef047712b5b7e6ae6c4621ea2cff3dabab4cd71c0bd559460e39d5f6e571006c3286bf72ec724e9bd5d9f8ccb0abd1557ef7408c3cc690e1e7ba8f4b0fb87d447b6e66cb34b90c3150ba3a2bd623fcc09ebefcabf62aa3bd01c63308c2428272ace8b01a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d69000000000000000000" + }, + "result": { + "gasUsed": "0x1b7284d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x957227ab77c764b14f4df2b5f2a60da060bd4da8f8f9f8ae87a67f2188f134a6", + "transactionPosition": 52 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0792", + "to": "0xff000000000000000000000000000000002d0798", + "gas": "0x5a9136f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821962b1590780a3e5852016727b45737aa29963044c46e1db6470429aacf34f13901c0f14737eefdbd5397827482d83c172d144687b9a88ea422d78dbbd983dfc149f18e56512cc94e94c81b34d5dab65edf4f2162c96a23f9a7e64389a6d5687930180c5777116996e4921375c5c20f43a201b95a204a461e4641476649e5cca5e164c74a8bd4a6598ded80a57ef8b1fba959de531e8a27a73a56ac06909e5a71852b04441cabb5675d2012b93285232c8e631d8bd76659134bcceabedadeca603df88a21c3db6640598a40b0df2473d6fc3869229a85efc0636dfedb4d34eca5b74904bbd5ed3af1cbb1f1a87643ed8414835ee9eb8a4825f62c74d7e750e5ec9fc5578548b78103536969c97d1f58ddf1acf39527e335827f8a78dd2d0a25419032819f99b03b66aa0fb3abb3f03068f34f03f0a8659bb6f1c16b80534eeacd0eaf88493a0ae91dfb25e5f06a4dd152b29a2c6498fb22755da0e514ab417656a676ca94d3b621810634ac162decd849c9488542d6d2fc73f293cd670884391b243947c42e6a310fca6b6f11683aea97b10d7040cbd42cd18c3c7b1f74ea9465037a82639898baf55c881719afd3b515668eaa6d42b8cf88178ec5656af49760c56f3384ca50305479afeb610aa3c73637f1b5f454a8eb775bee088a34fbf1008a303afd43d114b09ae243b41895bd07cba783646fc19a54f9e1026fc5ec214c6147795dcc334b6836bed237f75c03f70f81bea01c386742a25f408582d0cd4feee4b1d842aa45169e841cf4047bbb3774cf4e94989255905aad75a10ed978c9643994b39deaebb37a3a9d085a5161d592c536992b66e7ed7652b41676254a339e4cf630f7c1f8d067d558ea51a712e19f02829003589c84b321cd305f2b22dbf4777c6a04df233f59e49794948b97539b73bb1e801cd4c6d55b2462cee225939279de6b5d509a6c1ae5eec57cd9ef5eae92ecb9a6d83c1afce2371e1bb740975f4aeb53b57e6c8c3306d35541cd8bdb0e4ded06ff1aafa1fda68a352fa08ac3f2d3d878030af72503dc5edafa040e3f86d4dd393c84b75622c2579867f2aacf2b0fcc871d6a29ea710940a052fa9fcd7b88e61b629b41569616cf37756928a313e6c6e22e4e49ee9d5ff55bdae517a645d5e4ae3258b97737f906aa1f033d50e76e7d7994e9e2472ee6bfdb2b87ba54d0d85d81417674264deb6d747a2de5d3e8719ce7e6f0c510da24e1e3bb2c83810e9dfe19c5d69908183132dc368ecf12f57515b980bbf22bcf06fb059167910897ab200517795dcb31ffdb04466e78d000310859edb2e5cc76e85f6bbf79f1aba4667429b24b25e698ac37f20e24ac9fea4e7ebac408106c36d4939a70a027a838f3f561e41f3b084a773980f2803943d3145a5a5742fb373dd991e3b29f2746ee4820e19a1b52dbe9dac79b36204f52a85368cb507d93b7ec6a4935258ffacd2111459d57ed8b959e19627453384708da76ec1176404ad527184daee6523f76681a6958aa405e33d7335204474a9cc5ba2a7dbf1d556629c54ef419af663dc6c87b3166b1db21db1a772a94e119da2d83a9923bf6d08e50660df19f2ea19ba1408458ad5163911d0d5053993569f7c2feb04379c4db9cb6dc5bc2304138b9aafb4ae1165651a181bf252936af0c1450d9906bac56a379f27153b12424a645b5dc22f646e9483d5e9212adc5ef5755c491ee76be107d2cd0b0945a7d10aa5433be44fc36d806a83e064d88f9b1ffa85ca9428f1483c08ee4519fc35692df29f69fead901ba40966c150d96c54e7bfac3bd02a543d881d3609fb7d3956b942df9da3fe417ae9a6dcf2454eb7987ff5437f12cd8d77e15cdd292bd02d7480edf5b28dd6667ab3b0fbad18f5081b4171b6b3c92d8c3090b6d7f244272825fef7d2c3ba44c68ede30cdf681567a43a18d24d65948f09884933aa0eef904bb940b3dd1a92bfd67e6abc7eeb148aad962836b622007653043443474e44c2042c2e353bf99b556853d26a4d314e715bbbdc9073ffb98ec6cc40fa010c562c6b123ffff8cabfb1038a29a489899ef8d6dbe2031f073ab9f4d82582df6660dacd347dd6d207174f18db4977c669d3ae04f6d9fc5e1b190358c7155c1afb1f18e022d0207aaa724aaafc2484478ba1cd7abfb18046cf04a59eb36a313ab0e6cc23799943432d828ff486adcd3a6cd078cc8a931fcd0bf2c392e70daa6ac9c1def1e3a249fc6db7c451beab38b40738eba8e40bfc26b0b9580aacce29745135024154074bcd74774c8173f388b9a41ea5bbf36f8aec182dff1506f097f5a7aeb71749767b80b1ad9898d977c9f250439c8baf8114f94a7ec61f4ccd958f4680144ad1558c0e075d3b0d8a4aa38307191765bdadf1c62fb19b0e5b995bed7898195d30ddc393b2cd516976ea358e10882af8013947be9d0a82803e18d12c5f3d90e382a0d336413382e47014735c63bc04063941fdf5a8a5d1b57738a501ee1b50b185033926bc90480e0b8a991a693a610fa869245301c023fd291efb61b60d1af0f0f2e2080e1c17ee21ed1a4c6aa53c232949a1a0e2d325d0d3309c7b7de30811c7b1e2872918b3c2cc4f055e2b9afa7ade34b7462b95a8ed657248bd379407c57e0c0243aefdaa54b0597cc4206a85b6db894d534e00b91c07d7942e29107bb4dd5db5749e790e7a57bd23d50af35116d1c15f38e7ade6baa7000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x91d33d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x4de7b42d21f0a27c46254aa5536ddc1ba165322f56ad47655d256a8fdbcc0839", + "transactionPosition": 53 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0798", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x54822a8", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002d07981962b1811a045b2a6358203fd3e68cc4519573cd488191e2be8fb213cc957966f7ab315c24afd373103850582058eff1bd5d4366dbe11ce747891d3e24826b623b4459903b6ab586fda7f31209590780a3e5852016727b45737aa29963044c46e1db6470429aacf34f13901c0f14737eefdbd5397827482d83c172d144687b9a88ea422d78dbbd983dfc149f18e56512cc94e94c81b34d5dab65edf4f2162c96a23f9a7e64389a6d5687930180c5777116996e4921375c5c20f43a201b95a204a461e4641476649e5cca5e164c74a8bd4a6598ded80a57ef8b1fba959de531e8a27a73a56ac06909e5a71852b04441cabb5675d2012b93285232c8e631d8bd76659134bcceabedadeca603df88a21c3db6640598a40b0df2473d6fc3869229a85efc0636dfedb4d34eca5b74904bbd5ed3af1cbb1f1a87643ed8414835ee9eb8a4825f62c74d7e750e5ec9fc5578548b78103536969c97d1f58ddf1acf39527e335827f8a78dd2d0a25419032819f99b03b66aa0fb3abb3f03068f34f03f0a8659bb6f1c16b80534eeacd0eaf88493a0ae91dfb25e5f06a4dd152b29a2c6498fb22755da0e514ab417656a676ca94d3b621810634ac162decd849c9488542d6d2fc73f293cd670884391b243947c42e6a310fca6b6f11683aea97b10d7040cbd42cd18c3c7b1f74ea9465037a82639898baf55c881719afd3b515668eaa6d42b8cf88178ec5656af49760c56f3384ca50305479afeb610aa3c73637f1b5f454a8eb775bee088a34fbf1008a303afd43d114b09ae243b41895bd07cba783646fc19a54f9e1026fc5ec214c6147795dcc334b6836bed237f75c03f70f81bea01c386742a25f408582d0cd4feee4b1d842aa45169e841cf4047bbb3774cf4e94989255905aad75a10ed978c9643994b39deaebb37a3a9d085a5161d592c536992b66e7ed7652b41676254a339e4cf630f7c1f8d067d558ea51a712e19f02829003589c84b321cd305f2b22dbf4777c6a04df233f59e49794948b97539b73bb1e801cd4c6d55b2462cee225939279de6b5d509a6c1ae5eec57cd9ef5eae92ecb9a6d83c1afce2371e1bb740975f4aeb53b57e6c8c3306d35541cd8bdb0e4ded06ff1aafa1fda68a352fa08ac3f2d3d878030af72503dc5edafa040e3f86d4dd393c84b75622c2579867f2aacf2b0fcc871d6a29ea710940a052fa9fcd7b88e61b629b41569616cf37756928a313e6c6e22e4e49ee9d5ff55bdae517a645d5e4ae3258b97737f906aa1f033d50e76e7d7994e9e2472ee6bfdb2b87ba54d0d85d81417674264deb6d747a2de5d3e8719ce7e6f0c510da24e1e3bb2c83810e9dfe19c5d69908183132dc368ecf12f57515b980bbf22bcf06fb059167910897ab200517795dcb31ffdb04466e78d000310859edb2e5cc76e85f6bbf79f1aba4667429b24b25e698ac37f20e24ac9fea4e7ebac408106c36d4939a70a027a838f3f561e41f3b084a773980f2803943d3145a5a5742fb373dd991e3b29f2746ee4820e19a1b52dbe9dac79b36204f52a85368cb507d93b7ec6a4935258ffacd2111459d57ed8b959e19627453384708da76ec1176404ad527184daee6523f76681a6958aa405e33d7335204474a9cc5ba2a7dbf1d556629c54ef419af663dc6c87b3166b1db21db1a772a94e119da2d83a9923bf6d08e50660df19f2ea19ba1408458ad5163911d0d5053993569f7c2feb04379c4db9cb6dc5bc2304138b9aafb4ae1165651a181bf252936af0c1450d9906bac56a379f27153b12424a645b5dc22f646e9483d5e9212adc5ef5755c491ee76be107d2cd0b0945a7d10aa5433be44fc36d806a83e064d88f9b1ffa85ca9428f1483c08ee4519fc35692df29f69fead901ba40966c150d96c54e7bfac3bd02a543d881d3609fb7d3956b942df9da3fe417ae9a6dcf2454eb7987ff5437f12cd8d77e15cdd292bd02d7480edf5b28dd6667ab3b0fbad18f5081b4171b6b3c92d8c3090b6d7f244272825fef7d2c3ba44c68ede30cdf681567a43a18d24d65948f09884933aa0eef904bb940b3dd1a92bfd67e6abc7eeb148aad962836b622007653043443474e44c2042c2e353bf99b556853d26a4d314e715bbbdc9073ffb98ec6cc40fa010c562c6b123ffff8cabfb1038a29a489899ef8d6dbe2031f073ab9f4d82582df6660dacd347dd6d207174f18db4977c669d3ae04f6d9fc5e1b190358c7155c1afb1f18e022d0207aaa724aaafc2484478ba1cd7abfb18046cf04a59eb36a313ab0e6cc23799943432d828ff486adcd3a6cd078cc8a931fcd0bf2c392e70daa6ac9c1def1e3a249fc6db7c451beab38b40738eba8e40bfc26b0b9580aacce29745135024154074bcd74774c8173f388b9a41ea5bbf36f8aec182dff1506f097f5a7aeb71749767b80b1ad9898d977c9f250439c8baf8114f94a7ec61f4ccd958f4680144ad1558c0e075d3b0d8a4aa38307191765bdadf1c62fb19b0e5b995bed7898195d30ddc393b2cd516976ea358e10882af8013947be9d0a82803e18d12c5f3d90e382a0d336413382e47014735c63bc04063941fdf5a8a5d1b57738a501ee1b50b185033926bc90480e0b8a991a693a610fa869245301c023fd291efb61b60d1af0f0f2e2080e1c17ee21ed1a4c6aa53c232949a1a0e2d325d0d3309c7b7de30811c7b1e2872918b3c2cc4f055e2b9afa7ade34b7462b95a8ed657248bd379407c57e0c0243aefdaa54b0597cc4206a85b6db894d534e00b91c07d7942e29107bb4dd5db5749e790e7a57bd23d50af35116d1c15f38e7ade6baa70d82a5829000182e20381e802200c82b78d2956e18f54a7f16aeb3aad9d317d478d5b3212fefcc83d184999b859d82a5828000181e20392202096242ea129b2d38f1a44eb5dda71f9cd9f697abd137478959f807b834680c202000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3db1b4c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x4de7b42d21f0a27c46254aa5536ddc1ba165322f56ad47655d256a8fdbcc0839", + "transactionPosition": 53 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce389", + "to": "0xff000000000000000000000000000000002ce3be", + "gas": "0x2fb7db3", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708193efdd82a5829000182e20381e802207f4d07b116b6300ab7afb497429e9b845c6b81d1a4aa8b3f7dee0fb9c2d5ad5a1a0037e10e811a045365671a00480bc5d82a5828000181e203922020a2e68e2e80ebeaca7ed536bb450bb1c4a8577e2209135546f2287009cc45671b00000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2042bc7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8912c515d71922148eec6cd24d28673f3920b413bf2ced0ae6847f79c001d58e", + "transactionPosition": 54 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x2f0178c", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8912c515d71922148eec6cd24d28673f3920b413bf2ced0ae6847f79c001d58e", + "transactionPosition": 54 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x2df5244", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8912c515d71922148eec6cd24d28673f3920b413bf2ced0ae6847f79c001d58e", + "transactionPosition": 54 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2cd3cd3", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a00480bc5811a045365670000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x4888fe", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020a2e68e2e80ebeaca7ed536bb450bb1c4a8577e2209135546f2287009cc45671b000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x8912c515d71922148eec6cd24d28673f3920b413bf2ced0ae6847f79c001d58e", + "transactionPosition": 54 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0e9", + "to": "0xff000000000000000000000000000000002cf0eb", + "gas": "0x3f8e7b6", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708194760d82a5829000182e20381e802205bd2105f384ad53a57500cfda364fb6d48557fd75a9da12488bd97950b8e22031a0037e0db811a0453be491a00480bc7d82a5828000181e203922020133508dc0c6fc1cdcb063ef40e4cb11f4165e5d52f9bf41ca2f9ceb1f1e5701800000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2cee852", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf698e990a7ea335ddf046c92759be8d87ced39bae42e1295308137ce727281d5", + "transactionPosition": 55 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0eb", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x3ed818f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf698e990a7ea335ddf046c92759be8d87ced39bae42e1295308137ce727281d5", + "transactionPosition": 55 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0eb", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3dcbc47", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf698e990a7ea335ddf046c92759be8d87ced39bae42e1295308137ce727281d5", + "transactionPosition": 55 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0eb", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x3caa6d6", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a00480bc7811a0453be490000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x4887a6", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020133508dc0c6fc1cdcb063ef40e4cb11f4165e5d52f9bf41ca2f9ceb1f1e57018000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf698e990a7ea335ddf046c92759be8d87ced39bae42e1295308137ce727281d5", + "transactionPosition": 55 + }, + { + "type": "call", + "subtraces": 7, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bac42", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x13c70235", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000eb8181828bd82a5828000181e203922020edaa33053d8a5f916356a3ff18ad24fbda73e61e7d6bdd4486427e9bb61f180e1b0000000800000000f555010f29c20971f6e29cead00b57277fb5975fd83f914400a29f74783b6261666b7265696234616d7a696b6578787072753432747579626c7a35777736776636776a6236737935367a747863636b62696d647374627966711a003814d61a004f81164048001b6a51c29fe8e040584201667d08b321901df7ed569ed7f84c82d3b69c25b4bdee7b916eb19319500c778b06b89684eefd98e18bf8f5086c272b4c5fc53ac344a1b17e9d226612077b346f00000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x8c4173b", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000982811a045b5762410c0000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff000000000000000000000000000000001d0fa2", + "gas": "0x13b36e85", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000014c1cb970000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000054400c2d86e000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x13301e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x139f99ae", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x138ecdce", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 3 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000021f2a6", + "gas": "0x137bc7fd", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000ea825841667d08b321901df7ed569ed7f84c82d3b69c25b4bdee7b916eb19319500c778b06b89684eefd98e18bf8f5086c272b4c5fc53ac344a1b17e9d226612077b346f0058a48bd82a5828000181e203922020edaa33053d8a5f916356a3ff18ad24fbda73e61e7d6bdd4486427e9bb61f180e1b0000000800000000f555010f29c20971f6e29cead00b57277fb5975fd83f914400a29f74783b6261666b7265696234616d7a696b6578787072753432747579626c7a35777736776636776a6236737935367a747863636b62696d647374627966711a003814d61a004f81164048001b6a51c29fe8e04000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2e9853", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 4 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000007", + "gas": "0x126d96d9", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000c26ddbd50000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000064500a6e587010000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2ce23f", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000104f001206c3fe6bb46656ea1e89d8000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [ + 5 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000007", + "gas": "0x123e221e", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000d7d4deed000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000067844500a6e587014200064d006f05b59d3b20000000000000584d8281861a001d0fa2d82a5828000181e203922020edaa33053d8a5f916356a3ff18ad24fbda73e61e7d6bdd4486427e9bb61f180e1b00000008000000001a00176c401a001b60c01a003814d68000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x378d0ea", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043844f00120654f8b6172b36ea1e89d80000500006a8d15c1acd58b4e5110000000000520002f050b9c93842f9b9dcb15680000000004d83820180820080811a034d18b40000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 5, + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000007", + "to": "0xff00000000000000000000000000000000000006", + "gas": "0xf2d40e0", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000de180de300000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006e821a85223bdf5866861a0021f2a606054d006f05b59d3b20000000000000584d8281861a001d0fa2d82a5828000181e203922020edaa33053d8a5f916356a3ff18ad24fbda73e61e7d6bdd4486427e9bb61f180e1b00000008000000001a00176c401a001b60c01a003814d68040000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2439f4c", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000d83820180820080811a034d18b400000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 6 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000021f2a6", + "gas": "0x4c7ce06", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009c8258948bd82a5828000181e203922020edaa33053d8a5f916356a3ff18ad24fbda73e61e7d6bdd4486427e9bb61f180e1b0000000800000000f54500a6e587014400a29f74783b6261666b7265696234616d7a696b6578787072753432747579626c7a35777736776636776a6236737935367a747863636b62696d647374627966711a003814d61a004f81164048001b6a51c29fe8e0401a045b576200000000" + }, + "result": { + "gasUsed": "0x9858a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe62a29767a0e1030fd633df1090900a6ca3b2ccfde69682b8b4850a4faa385ad", + "transactionPosition": 56 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cbdce", + "to": "0xff000000000000000000000000000000002cbe59", + "gas": "0x5b6dcdd", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782196cb85907808da266f0d295889a05be62ca726ac93cae2bf1976516514387a04f6c161b95f727f97fe04494e7eed2533dfa309ff933835dfcf8a37564e3ebf5e4f2a7c0048aa707c303814028b15d44a34184d6e88f422defb35d333642a4592eee09398954097c2bb5290d2d989947f61ffcfbb57ff4efd12c5f0bdbe44e97487e45aeca92cdfd5678303f0b20617dc9fb65ecc734986207f6e60aeb1426702cebcba9b1e3e99b1f05d922b877818dcea601b115ee1794c04f76b666de9bf218f78f3bcb44b1c2f38368f86d53ce36b72bf6517d1a5f289da6799cbc2619403ea09b6f2fe13d598586938debb8a2e28762e65540b2b5401175c6e0533f20065b308444fcdd2462733270fba4adff7c4f1e9ac2c311b0c4e2fc6304f26ad4f1700a849eecaa0778bc7133031c1de707450c8fb1dc14f13789ed53844ca17939d2bc948ec550d0c18282b578e1228c7fa698c47611c28933cc0ae919ea14bb1aa6f75a01e7c474660eb686047eba0742f543acce9640bb67b163874d4528c7ea10b78fbd2a9180d69cb3e6477e8076fbfc1331194b04165be5e67f5161067e3b462edae40b9696d0d5217a1c20752df202384733c238a2a0b1ed16038f3cc7cbe51848d47d9031302870450538e8dd0022e41fb115ef3a4d9c4581a9229ab5385dce0753fcde17e8ed5bd77e3d3f865bc9f2da86ddff535786f72c93627459d5e4953019b9e71a0a13ff2fb7745b9a770c2ff2147edc8bb388730ff2c7b60645eb02a3dd96c6987d5cfe6f2fc254916d37c4589807faccdeed6595c5ed36d3bee947f5600f74b7737eaf6da28e3ba27ef9bd8b2075819620049af7b0034a55355a032fd807838dde66f7e9fee2aafa916f8c481bc8fca7fd868e1b39433a55bb96819eade540d63a2541f6de1ec9bca1c9f5545ab9b87481738c6868c3173daa50e42d6ca2c70172e512f1a7716ad8fcaa273d14d22466de3b18d9ac596b99fbfe1b50dc78a43042057415f19d79a9fb0243639303eeaccd138b5e8ffd0eaa1afa6418b7acab26e09458079a866b5cb655e0e2c9193a3be567ebdb8be7aa7b7ec4ca5726490798ff39a69bc1d15c8b4db2258173bb90c15ccdd3a38f0761a67a2893b7a176478345af99a4f151d89bf47addc9c19dc0a1fc7b8cfc2777cf6f98690feb3024ee5e393f63430d96d88efd01077fb4793b1a1369c529f05ccb8631a86eda1496c700095d52127504428f372d5dfd5b0c62aeed47698c64685eabd5ca056c3cd59f1c352de9b6aaa4ad2e3cfc3ea8ca119db8b24685432c4b2e6904c8b2d9ebc40585f8e37d77d585bc45b24448685f85b9061254e159b4d19ea81a33a3ee180e1ead0492e74fc2f4223ab2fda859b7c5e5abeb763194281c9b80ec89d0e1ef85bb4cbeb34dd238cc85f4f4b916ff8a3b47a9a18925f84adbf8b425970f6b2fa58789473fa2092f619365699266bdf2c84d80fa2f580f2135ea744abe1d6522a98b018b0c48e390f635a4e53c8df2c17b981d8e9b9ecf20681cbe9b727ec422a107486effcc2eb1255b4b16dd9ffda0d317834a3dcaba0647ffff274bbafb78225ed55d8a91e5fd5593425ec96db174708fae8ef62c34bc529fbe0ce13b52fb99d09288a63914fa099634fcc6af402b29dec3579afaa5136ae438ad32576e0a401d9e9da4dd79dd76992b02dc924d7b83c8b1f01ca4409fe937525b7772962430e8169a122afe5535e4c05af5001c96c548ddcdfbf2e313846f4422efc1b7d6399f022926f0a45475d6a1fc2e8dc9e1da4ceeb78ad725f324a5a495e4af104b244d3aecad990a3bf0538bc13afd284af720833bf6e0be96192104f181450c28bb8da27e81dda480c36dc01f13a33989f2cbeb16b295fe2b2fcc6f964fec34622e66a1ce4cd850d06e17ba099287e812603fdc936768fc2f0d13256fb56cd98b546906dd67c8f9c3724429cd8df899588c71a08404ad8e7443b3d19d3614fc7be29a6c7ed83efbf70d3d642c4673ef2622f45fa6ca7cfba8d0c1c49b40caf850a218158cceea53ed7379476f03114197df49e352e97677764c0d050c24155edfc4d1089bafcd0666161acc6f2b53bad7648f9564a9cdc969b22d769aa037c00b4b27aae14db8d6085c48ec89f16ef310c532d483aa1754e0e4b1d3f50cc91ef92bb8af19f71eeead2c2a1b32f5f8554204d22d23c1e861b8d3b72b38391abbd4c9a468c75f9f55425b1ccdc7d75fcd8f0730aa27d634fb3347a5a79bd73bbc4d8f0c3b2ad4900a618b54a9fe84738413ae34ccadd19059d68c869976f3b1342b6d53197b798f65977ad905386fca03705d04e6e826b343229a267164b2068d851c69f560abbdbb426aa9ebfd2e79d1e5ea05b8b78869b7722b5245a91021cec3673336befd9074f10aa72cb164cc46be41573d5f482e0495a8287951c9e85dde7e9494044ad93f6981796257b8b6f4095dbd21d9dd853506dd43bc225116cdb4b019d74a49ccd953656d12162103019cdc4ab77f2248b477c0584848da3e7f70c61eb4e66e01f49b97ec92190ca44cf1f45753ddb1d135f00074b70ca728b939379102b396420ce758f0bbae79b2809293d9161621b3a3ead8574652a49f857aedc874aafa48269d666f46efd63c17100812b9a79a0500309e82d3bbe467996dda0aac3afc8d391c6cd3a55d7cf2c7daad4da2072c6800b7d1fc9273921bb6ca97a400000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x9e78d2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x19d4af3d4de075052a7e46d1e79cdac48c06b20a637af57edfaa39add1b7ebbf", + "transactionPosition": 57 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cbe59", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x5493bc3", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002cbe59196cb8811a0458d4515820c733511a1ee67c4b6354648a0aad557a7d6103db91ba7ec3605d0e0d0a7dd727582070588192c2cfcb22bd6934b66eb4b782332859eca0353d7a6a6de5ae787e23ff5907808da266f0d295889a05be62ca726ac93cae2bf1976516514387a04f6c161b95f727f97fe04494e7eed2533dfa309ff933835dfcf8a37564e3ebf5e4f2a7c0048aa707c303814028b15d44a34184d6e88f422defb35d333642a4592eee09398954097c2bb5290d2d989947f61ffcfbb57ff4efd12c5f0bdbe44e97487e45aeca92cdfd5678303f0b20617dc9fb65ecc734986207f6e60aeb1426702cebcba9b1e3e99b1f05d922b877818dcea601b115ee1794c04f76b666de9bf218f78f3bcb44b1c2f38368f86d53ce36b72bf6517d1a5f289da6799cbc2619403ea09b6f2fe13d598586938debb8a2e28762e65540b2b5401175c6e0533f20065b308444fcdd2462733270fba4adff7c4f1e9ac2c311b0c4e2fc6304f26ad4f1700a849eecaa0778bc7133031c1de707450c8fb1dc14f13789ed53844ca17939d2bc948ec550d0c18282b578e1228c7fa698c47611c28933cc0ae919ea14bb1aa6f75a01e7c474660eb686047eba0742f543acce9640bb67b163874d4528c7ea10b78fbd2a9180d69cb3e6477e8076fbfc1331194b04165be5e67f5161067e3b462edae40b9696d0d5217a1c20752df202384733c238a2a0b1ed16038f3cc7cbe51848d47d9031302870450538e8dd0022e41fb115ef3a4d9c4581a9229ab5385dce0753fcde17e8ed5bd77e3d3f865bc9f2da86ddff535786f72c93627459d5e4953019b9e71a0a13ff2fb7745b9a770c2ff2147edc8bb388730ff2c7b60645eb02a3dd96c6987d5cfe6f2fc254916d37c4589807faccdeed6595c5ed36d3bee947f5600f74b7737eaf6da28e3ba27ef9bd8b2075819620049af7b0034a55355a032fd807838dde66f7e9fee2aafa916f8c481bc8fca7fd868e1b39433a55bb96819eade540d63a2541f6de1ec9bca1c9f5545ab9b87481738c6868c3173daa50e42d6ca2c70172e512f1a7716ad8fcaa273d14d22466de3b18d9ac596b99fbfe1b50dc78a43042057415f19d79a9fb0243639303eeaccd138b5e8ffd0eaa1afa6418b7acab26e09458079a866b5cb655e0e2c9193a3be567ebdb8be7aa7b7ec4ca5726490798ff39a69bc1d15c8b4db2258173bb90c15ccdd3a38f0761a67a2893b7a176478345af99a4f151d89bf47addc9c19dc0a1fc7b8cfc2777cf6f98690feb3024ee5e393f63430d96d88efd01077fb4793b1a1369c529f05ccb8631a86eda1496c700095d52127504428f372d5dfd5b0c62aeed47698c64685eabd5ca056c3cd59f1c352de9b6aaa4ad2e3cfc3ea8ca119db8b24685432c4b2e6904c8b2d9ebc40585f8e37d77d585bc45b24448685f85b9061254e159b4d19ea81a33a3ee180e1ead0492e74fc2f4223ab2fda859b7c5e5abeb763194281c9b80ec89d0e1ef85bb4cbeb34dd238cc85f4f4b916ff8a3b47a9a18925f84adbf8b425970f6b2fa58789473fa2092f619365699266bdf2c84d80fa2f580f2135ea744abe1d6522a98b018b0c48e390f635a4e53c8df2c17b981d8e9b9ecf20681cbe9b727ec422a107486effcc2eb1255b4b16dd9ffda0d317834a3dcaba0647ffff274bbafb78225ed55d8a91e5fd5593425ec96db174708fae8ef62c34bc529fbe0ce13b52fb99d09288a63914fa099634fcc6af402b29dec3579afaa5136ae438ad32576e0a401d9e9da4dd79dd76992b02dc924d7b83c8b1f01ca4409fe937525b7772962430e8169a122afe5535e4c05af5001c96c548ddcdfbf2e313846f4422efc1b7d6399f022926f0a45475d6a1fc2e8dc9e1da4ceeb78ad725f324a5a495e4af104b244d3aecad990a3bf0538bc13afd284af720833bf6e0be96192104f181450c28bb8da27e81dda480c36dc01f13a33989f2cbeb16b295fe2b2fcc6f964fec34622e66a1ce4cd850d06e17ba099287e812603fdc936768fc2f0d13256fb56cd98b546906dd67c8f9c3724429cd8df899588c71a08404ad8e7443b3d19d3614fc7be29a6c7ed83efbf70d3d642c4673ef2622f45fa6ca7cfba8d0c1c49b40caf850a218158cceea53ed7379476f03114197df49e352e97677764c0d050c24155edfc4d1089bafcd0666161acc6f2b53bad7648f9564a9cdc969b22d769aa037c00b4b27aae14db8d6085c48ec89f16ef310c532d483aa1754e0e4b1d3f50cc91ef92bb8af19f71eeead2c2a1b32f5f8554204d22d23c1e861b8d3b72b38391abbd4c9a468c75f9f55425b1ccdc7d75fcd8f0730aa27d634fb3347a5a79bd73bbc4d8f0c3b2ad4900a618b54a9fe84738413ae34ccadd19059d68c869976f3b1342b6d53197b798f65977ad905386fca03705d04e6e826b343229a267164b2068d851c69f560abbdbb426aa9ebfd2e79d1e5ea05b8b78869b7722b5245a91021cec3673336befd9074f10aa72cb164cc46be41573d5f482e0495a8287951c9e85dde7e9494044ad93f6981796257b8b6f4095dbd21d9dd853506dd43bc225116cdb4b019d74a49ccd953656d12162103019cdc4ab77f2248b477c0584848da3e7f70c61eb4e66e01f49b97ec92190ca44cf1f45753ddb1d135f00074b70ca728b939379102b396420ce758f0bbae79b2809293d9161621b3a3ead8574652a49f857aedc874aafa48269d666f46efd63c17100812b9a79a0500309e82d3bbe467996dda0aac3afc8d391c6cd3a55d7cf2c7daad4da2072c6800b7d1fc9273921bb6ca97a4d82a5829000182e20381e80220c655d56cf0ef0742b187ef633a957ffbd32c3defade8f2594ec34910ae8ef12cd82a5828000181e20392202097b123d3710a5dd973f857a5dc4d0514f0a97e1d975eb01e42aaa1cd87b10e3f000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2fbb841", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x19d4af3d4de075052a7e46d1e79cdac48c06b20a637af57edfaa39add1b7ebbf", + "transactionPosition": 57 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001a0f6b", + "to": "0xff000000000000000000000000000000001a0aaa", + "gas": "0x1c23858", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285098182004081820d58c095c1f3fcb3c1579274a763a69efe68df562832d6d07902f5bd0d92df6c5f3633242ab04b3bf33cd6c1dc4e53e6348e57b64a9c8af60ecc295357de84447d6eec698cf27d62e260272bb587d433ca022f29db4399a700813df8341aafefde3eac07783ba253600998029028668b87cd91de868c4a1e0485ecdce958493fb7b03de06060e93b360705af0f49e300d06c90acdf3e6e8d6de2933463909e42a9f1b6a96a465cab34d14c762e113b2ab20797a9101fa6321983e962c74fdab9fe9eda1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x17514ec", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xe85fd747a88049a644b187321afe6fc108862c99c3e42fb076fe35e266905bcf", + "transactionPosition": 58 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c3bf7", + "to": "0xff000000000000000000000000000000001c3c0e", + "gas": "0x6e1b2cc", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000027a8518258382034082044082054081820d590240a06d5c12f5ddfcf4805175f47f5711e5e0eda838d6ccc4423f9426fa01451a3037af6100b3ea8913e16caffeac1f8308805c1898f39f306d98d01c0cde75312cd842c524eabd33bd58aa15b50f9d0b1f2e068c36ce8fb4db6cd66e4f11ebb93c01d973c053d13e1b070b9ffe554b0ff62e602e7940bd9904618ab4bdac3dadcccd10183165128f991bc97d20924c719e8b483140672040df267d9c5ec364f04c5df0b2134c18673cb86feeae16bad31fc27d68282f9a2158b0d91718020cf98383682293553b37a85872eaff3c7b34cd107853c65f24bcbbee7c0962080abd615c7568f4f84a795e9c43e21bfd10d3008efcab45e9f96671fa38198fbd4d4f9e50e9285ab59092b4c9f7f34936dab4996949b05ecc76a489b2e2cc02943f12fb143cbbfada5ddb42821e823f5c8acb9686de0e3ef85fc41d39719dbe8702ae9a3662a834f68c200c79f6d9e7c7032124b438ab8660ac3de175f80c0c7fefd992e83534f7b1f7e7c3afb4974cebde8238ffd9f3a738047ee515d483b2f91fd8fc8630777082c23784590b3d8a817a963f10a39e83a5740f7b6954a8930dd713c6fa332ba0da90a8e2b3f71e645f6876ac8a2b980be79ec5fc0d39fec3ac0a6cbcb99e661b4d62d5a46d6feebe18280aeb0e2ff21001b3bdcf4faaa677f16dbd9a03d0dd3fbec38d38403f21fe7795a4612bfec76bb2c945456637b84d2929e0d962a79a161de51d0ff523b91c342c37e5b170338c7678717aecc762d5935ffd168257763650220d5f38f6a269d2c39160a243cd163f8446607bbba2f2d5e61aa91a0037e5de58201bc00903e90ca71cf8a383db0b4d3a038e6c65f02639a026af83a66ff71862a5000000000000" + }, + "result": { + "gasUsed": "0x5b37ee5", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x468720813c31079df1c1d52f6bdc98c5b55dd3c3cf0c0f5377d692e2030c0ed8", + "transactionPosition": 59 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c3bf7", + "to": "0xff000000000000000000000000000000001c3c0e", + "gas": "0x548fbe8", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f38518258182064081820d58c0b4a54e9a1ab64f94e7977377bea362e540a83376e284bf86dc426fdde3652582267f7508e553394e0317f636155ad659b04b297544b50bc0ddb85e23b7657fb6851233d15ed8c422341192b767e9627396f6b4c1380db51647bda0570fc0c8f51016b9cf8092e073beb73a451a93e08c309762bdd09ab92af5c738661bfdd8e86eb24c5ddbe399bf534221d26bd3ac0aa247b059202ec208dc4b57488ed12e0144b3ab60c54912333580b0b5e2c20cf0f14908dfb11b7514cfd0624a6ccb4c591a0037e5de58201bc00903e90ca71cf8a383db0b4d3a038e6c65f02639a026af83a66ff71862a500000000000000000000000000" + }, + "result": { + "gasUsed": "0x4840f87", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x6ae23d16858a9de909457b27a4a7272b0b7bee42f8d5f12c10f0cdc55118b459", + "transactionPosition": 60 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3338", + "to": "0xff000000000000000000000000000000002b3363", + "gas": "0x4ae7ef5", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000072818187081a00011b51d82a5829000182e20381e80220f29dcf0ce726c36a3a9dec02c7975bc7d4fbe9058f088a5f66d827231404b12d1a0037de15811a045af8041a004f7082d82a5828000181e20392202091de727d20058c198e55b1a08967352ff72168b2b70f0c597e7e907b3778df3a0000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3613dfc", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x558e1f235e7b5c9ee2373bb7c39b2c236f584979cb6d3b0898e1e283e3c05e07", + "transactionPosition": 61 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3363", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x4a318a5", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x558e1f235e7b5c9ee2373bb7c39b2c236f584979cb6d3b0898e1e283e3c05e07", + "transactionPosition": 61 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3363", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x492535d", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x558e1f235e7b5c9ee2373bb7c39b2c236f584979cb6d3b0898e1e283e3c05e07", + "transactionPosition": 61 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3363", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x4803dec", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f7082811a045af8040000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x489183", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e20392202091de727d20058c198e55b1a08967352ff72168b2b70f0c597e7e907b3778df3a000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x558e1f235e7b5c9ee2373bb7c39b2c236f584979cb6d3b0898e1e283e3c05e07", + "transactionPosition": 61 + }, + { + "type": "call", + "subtraces": 4, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b199b", + "to": "0xff000000000000000000000000000000002b19a2", + "gas": "0x4ed0b83", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022881858708195f5bd82a5829000182e20381e80220f9acb798b6fe4fb2b334af3d3e0ee522004ea693556ed6e7ead3d68306d75b151a0037e089811a0457ca271a004f598cd82a5828000181e2039220202e5970c0e581497fbb90cc3f526c0d9d553c5fbcdb2aaadcb64149ad5f22b0098708195f37d82a5829000182e20381e802208fe0b57c9c0dff7538fdeedef16dca53661ce854dc7a06a296a6adc3491ce7361a0037ddee811a0457c65a1a004f598dd82a5828000181e203922020607d2d181747f8a1df3b139e06f52588adf196097d38f13f8160aed6d437e1188708195f5cd82a5829000182e20381e80220c31fbd6bb96206aaf825a12337ddcac48808b5b835ef8d2b1c6b82255f1b0e081a0037e0a6811a0457f0331a004f5983d82a5828000181e20392202036078ad115da0b077798bbffb9abdf4756c5fb3eba49d917f608f4e93791b13b8708195f38d82a5829000182e20381e802206a13d744c58c3c3194185fc76b711ddd793451a6bc1c1b0d2593391fa4b9aa271a0037ddf7811a045921771a004f5980d82a5828000181e20392202085574c49ce6d1f8149b4631ce8d502e8764c0e1a949f05287f34553420e6ab228708195f5dd82a5829000182e20381e8022037ea5178bc9933cd3e272574beb4420d23ba7fd1141d27ef728aa2a03cb78d1d1a0037e0aa811a0457e4e41a004f5987d82a5828000181e203922020f4abd76e214c8125bd6b5470b3d111cc049db132b3e80cf0d9e64beb13cd4129000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x307c5da", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xef58a04d45dda0091572a6ae13c1c043e2397ab851d94fb07f108d5bc34beac5", + "transactionPosition": 62 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b19a2", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x4e01c98", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xef58a04d45dda0091572a6ae13c1c043e2397ab851d94fb07f108d5bc34beac5", + "transactionPosition": 62 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b19a2", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4cf5750", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xef58a04d45dda0091572a6ae13c1c043e2397ab851d94fb07f108d5bc34beac5", + "transactionPosition": 62 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b19a2", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x4bce48f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043818583081a004f598c811a0457ca2783081a004f598d811a0457c65a83081a004f5983811a0457f03383081a004f5980811a0459217783081a004f5987811a0457e4e40000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xdad3b5", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000de8185d82a5828000181e2039220202e5970c0e581497fbb90cc3f526c0d9d553c5fbcdb2aaadcb64149ad5f22b009d82a5828000181e203922020607d2d181747f8a1df3b139e06f52588adf196097d38f13f8160aed6d437e118d82a5828000181e20392202036078ad115da0b077798bbffb9abdf4756c5fb3eba49d917f608f4e93791b13bd82a5828000181e20392202085574c49ce6d1f8149b4631ce8d502e8764c0e1a949f05287f34553420e6ab22d82a5828000181e203922020f4abd76e214c8125bd6b5470b3d111cc049db132b3e80cf0d9e64beb13cd41290000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xef58a04d45dda0091572a6ae13c1c043e2397ab851d94fb07f108d5bc34beac5", + "transactionPosition": 62 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 3 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b19a2", + "to": "0xff00000000000000000000000000000000000063", + "gas": "0x1053f8b", + "value": "0x48fa86c15fa600", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1770", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xef58a04d45dda0091572a6ae13c1c043e2397ab851d94fb07f108d5bc34beac5", + "transactionPosition": 62 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001614ac", + "to": "0xff00000000000000000000000000000000018a9d", + "gas": "0x2950360", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285028182034081820d58c0a24c0e37761bfe50b11dd0fd147cc58de9c706966e58ebf1c59bc68d826c3aa1c8ce21e214b65146eebb6599c7e9b13e9771b9703259e98da1c6be2581c7a3d32de9391b77cba94683e4a488a85b7677f7c4b24ee043e5b74328c64b2ab3548e168389d0eae2771b996bd858db8331aea66eacc1dab69bd7488c341a69d1a77c19373ab2cd2e0a213a4dd87e3a65b47ab6ebe365a6b834dd6d0f508e3cf61f4d534aafcc397017d2858c44e26b493131f8db7feb49cdc4df3f916b2681d5a8701a0037e5f6582080334418ee2e59dadfdb8d33bf996c9c35f36dbe740e214e308a34ebd796ddc90000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2288efe", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x9821a2e7145a9a0635ef1ef3b683392817d43c61caafbfbff0ec5422c3ada2bd", + "transactionPosition": 63 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b199b", + "to": "0xff000000000000000000000000000000002b19a2", + "gas": "0x43cf604", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782195f2a590780af0a4181a3dd50f9927739353d495ca29562a915a1ac79b0a82ac9fc0a2f6cf9a54d632c8c9f0198f2837f66507ce46eadce1f4c166c6256647d8973789a927937e5e74d9a27305ce146b549af4be391a9ae468e2a579b2864a66e7f641ffa38019d7736b70736af2d3747d2b5baef4a5d594ff32fec3d286dc115f7af556287d19df302659af6dbc84444e43eb9afe1a42de2f425ff8d15f75809e36e3d559197df381cbaf4cab36ac23c51ae9fc19c0e9dd92ce497c3757784cdde3d11c766a8a3142e9a8ddf1c5c03f6cb797b60a407d6b70395c40be27bca492c50f56b990468a1a436d4d84109afed2b7a8ace86b7c4376ac7e99bf30ef9360b4f84cfe243c8808e68fdc48d6a4780c225d78fe033eadfd2194bff4b72dd07e0c763ce8d13687ceddb0fe5ddcbf00a8d3ef2795a04928c1cd2c8ab9883d8b027d6b9d2e7130732f84d2e7677e8a66fc09bf7677fa8e5a83b4318d7991fa6e9c7e4ed85d92f4d0aa0421ca497c5248e7740b133558b3f348254b784271ea1cba40c570c6ba6dffaf4a6aa9c7f7f0c32d3175cd4b3ecacc8ec238a0b2dcd12decb7d6915f04cd722fe3c39385a51e4fd5a62663027a8e0e9d833d18e95f375aaecac522193cd514123eb9e351fc001127196a777d6cf7a47e490d38bcc435539a21bb781770ebef5972953b9793169212f89ebf0e717d9e37985df6313884a24442391e52cec8e668ffa90cc2f87e4a0ed3d4358d3b8d1ab4702b58df778b1710de8d416f6249f10524144721304e415dcb2b41599553e111f80262229c3e3ef6d2cd9c0a0a34efc99920676c84381dcc23180cc9094bbd90c29caa08077a6dca013e081c38db3db3d69dc436fe177d940ae47f566a2326d7b901bf823aae9ada3d119291b1d75eb7e327b19b4bec1217b732fad8c4c0a68bb8238fc3b9ee2ebc78d295773030cb0c8526ed6426f0765b5b1f1e89d0d80dc399f99ad18b4fb29e1236babcf994e75f7f60a68cd364bada4a5765af2b50f4b3a4695b3fdae7c48133f80fa6148b12478a1b46810d31258b50e3a694d6817548a39d664ba0d5fdda0d32f458b91119ff404818d4163345ac7fcb4dbff87b2c20d407c829c200236f9453a43a8bc9cde4da3d3a6763d306e86f97cf996b875e446091b221926e6a3574a64e8eed09a03e8450707c097c5059b357df5d2625446ebb495637f2137565d97a6e919006a576f55c4eccae9d0919dcc15984d908f970d6988fdd9df8c505c0f1c078069a659c07c420063bd90c71d4b9b4d16908921063c78e8b88fcd606b8505e4cb4c0a23983090df8fa3b14176c08c293c9404a2f3844a16eb4c3d2542398cf90db898fdf234259a06f52e445e2ef8755db263a09c97abb53b6c9044fb2214fbbeeaff34c0888b8ae6977a082b521a106aacaab8acd1faa0422a83a4fd799d3f3bd384bc017b5c2e2d00cd16ebdc2b41dcf1f5f41886ae1455fe0cb8769066a4f114205ed50579936c6ad6ee817a47080865f04c33bf029acbedeedf5474f90e9e556cfebb52bb44b745ac3d1119980a2998282e3384b4b4b72e44a2ce7715853f9f8b4289d83c5a524d0fde04ae762a1e6b9a25bf1a6ea57eded7737afca6b61e83639136b89ce030c6c54cedb8392db55895af9ad866bde97db08fb12f9f235e296c86fa007954cce91f615ff01ad1d7aa0ec788362313392bd4d47921872d821571d42689c2d10e9f27e30e065b0f1c03643f419b5ca7aa5157373e6c0b8ae60b5a56daa8932782bcb72bd7da154719fa551a84af6e3f7b5e30f263944c6c1f47fa510d037b90e01b1095df28baf2ef9885b7af50aaab01ccd913b9f1dccfec40b7fe6c4aefd442654aaa5442abd774d41d55583656a53d5bc9f3ff08d87478894a9ddecd39efa5f2119bfb25415970408fcf3265a8faf706bf6841953e44371630048ab9b604354fa279314cd9dd19af0822942179f32bcd9f9d1b19e4ec3ad7308372d8b5bf03908178e5d4c5e87a3eb40c0120fccad46d4d0f91c484416e0132a4cc9dde03bd5d4c492aaa06967014c5b414c8fedb95c4553f69643e6bdcf3076a67a85b8c32ae6a0e72ce8d2737a8724688121662677175c2ac618a2c481cd4043ce6c0fcd6a1d9bb39e1ac0caf63d27b1a01366de0f5f93d007f480efc95b00263a391ba807aed88a3e421ab2d62b9e604456026434da79a0949a7b12f9c791ddf565ea9f107b4c1b8050d563ba5a633528f068dc30cf7a440cfe418de1f54c571ac670cc2a2fb17ee44e9d57f9c7cdadf0b7772bba6edb8a6b260631a01532da47bf3fc27fd5b137a4a449e0a25a4adb6b60eadd2ec7abec7e32f970fe50696932a324eb8a81297a23f6e27d082d3e7bb299625f591c887bcef5587fdfaad00d5e1faf2c1c17dd48b0504993f224285f3c40e8965bc39422455ff1e248a1ba8859079c361dd332b2246b733cf51cb21b38f704127a36cf08f92b9200a3f951b90979e061b5b3b3c7163fbb03eab9ec644a1f14b05c3e4d90ec541006d90c315a69a4bcbebba3bfb5a1be8814bf3006cdbfc7436673e967e1d2d0be00f12c2ec5b7fc3d05ae6d23cd318c77f9ba86f31dafd6c886a3bd0e05dbbaf6dd052c29e10c13355768314c14c1077f3e8ada461e49d3bb8e0d9270b25f1e35bc638e1c732188ff5a7e082cfd91183afdc79ecc78878e1e0122abf3b767051b93600000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x841db2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x352d011f8bc53595968e076db6f8bd657486a8fea38439ae699980096c2dc7e9", + "transactionPosition": 64 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b19a2", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3e9b138", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002b19a2195f2a811a0458bf95582019d87ad3ccfc90bcec5bdd6e8270ed89d169ef967b5f727bae65de6ef176ae9b582045d1743b2baf96952374e2de1c0f2ef15e7cf0e678ebb9d158d2572d304a8530590780af0a4181a3dd50f9927739353d495ca29562a915a1ac79b0a82ac9fc0a2f6cf9a54d632c8c9f0198f2837f66507ce46eadce1f4c166c6256647d8973789a927937e5e74d9a27305ce146b549af4be391a9ae468e2a579b2864a66e7f641ffa38019d7736b70736af2d3747d2b5baef4a5d594ff32fec3d286dc115f7af556287d19df302659af6dbc84444e43eb9afe1a42de2f425ff8d15f75809e36e3d559197df381cbaf4cab36ac23c51ae9fc19c0e9dd92ce497c3757784cdde3d11c766a8a3142e9a8ddf1c5c03f6cb797b60a407d6b70395c40be27bca492c50f56b990468a1a436d4d84109afed2b7a8ace86b7c4376ac7e99bf30ef9360b4f84cfe243c8808e68fdc48d6a4780c225d78fe033eadfd2194bff4b72dd07e0c763ce8d13687ceddb0fe5ddcbf00a8d3ef2795a04928c1cd2c8ab9883d8b027d6b9d2e7130732f84d2e7677e8a66fc09bf7677fa8e5a83b4318d7991fa6e9c7e4ed85d92f4d0aa0421ca497c5248e7740b133558b3f348254b784271ea1cba40c570c6ba6dffaf4a6aa9c7f7f0c32d3175cd4b3ecacc8ec238a0b2dcd12decb7d6915f04cd722fe3c39385a51e4fd5a62663027a8e0e9d833d18e95f375aaecac522193cd514123eb9e351fc001127196a777d6cf7a47e490d38bcc435539a21bb781770ebef5972953b9793169212f89ebf0e717d9e37985df6313884a24442391e52cec8e668ffa90cc2f87e4a0ed3d4358d3b8d1ab4702b58df778b1710de8d416f6249f10524144721304e415dcb2b41599553e111f80262229c3e3ef6d2cd9c0a0a34efc99920676c84381dcc23180cc9094bbd90c29caa08077a6dca013e081c38db3db3d69dc436fe177d940ae47f566a2326d7b901bf823aae9ada3d119291b1d75eb7e327b19b4bec1217b732fad8c4c0a68bb8238fc3b9ee2ebc78d295773030cb0c8526ed6426f0765b5b1f1e89d0d80dc399f99ad18b4fb29e1236babcf994e75f7f60a68cd364bada4a5765af2b50f4b3a4695b3fdae7c48133f80fa6148b12478a1b46810d31258b50e3a694d6817548a39d664ba0d5fdda0d32f458b91119ff404818d4163345ac7fcb4dbff87b2c20d407c829c200236f9453a43a8bc9cde4da3d3a6763d306e86f97cf996b875e446091b221926e6a3574a64e8eed09a03e8450707c097c5059b357df5d2625446ebb495637f2137565d97a6e919006a576f55c4eccae9d0919dcc15984d908f970d6988fdd9df8c505c0f1c078069a659c07c420063bd90c71d4b9b4d16908921063c78e8b88fcd606b8505e4cb4c0a23983090df8fa3b14176c08c293c9404a2f3844a16eb4c3d2542398cf90db898fdf234259a06f52e445e2ef8755db263a09c97abb53b6c9044fb2214fbbeeaff34c0888b8ae6977a082b521a106aacaab8acd1faa0422a83a4fd799d3f3bd384bc017b5c2e2d00cd16ebdc2b41dcf1f5f41886ae1455fe0cb8769066a4f114205ed50579936c6ad6ee817a47080865f04c33bf029acbedeedf5474f90e9e556cfebb52bb44b745ac3d1119980a2998282e3384b4b4b72e44a2ce7715853f9f8b4289d83c5a524d0fde04ae762a1e6b9a25bf1a6ea57eded7737afca6b61e83639136b89ce030c6c54cedb8392db55895af9ad866bde97db08fb12f9f235e296c86fa007954cce91f615ff01ad1d7aa0ec788362313392bd4d47921872d821571d42689c2d10e9f27e30e065b0f1c03643f419b5ca7aa5157373e6c0b8ae60b5a56daa8932782bcb72bd7da154719fa551a84af6e3f7b5e30f263944c6c1f47fa510d037b90e01b1095df28baf2ef9885b7af50aaab01ccd913b9f1dccfec40b7fe6c4aefd442654aaa5442abd774d41d55583656a53d5bc9f3ff08d87478894a9ddecd39efa5f2119bfb25415970408fcf3265a8faf706bf6841953e44371630048ab9b604354fa279314cd9dd19af0822942179f32bcd9f9d1b19e4ec3ad7308372d8b5bf03908178e5d4c5e87a3eb40c0120fccad46d4d0f91c484416e0132a4cc9dde03bd5d4c492aaa06967014c5b414c8fedb95c4553f69643e6bdcf3076a67a85b8c32ae6a0e72ce8d2737a8724688121662677175c2ac618a2c481cd4043ce6c0fcd6a1d9bb39e1ac0caf63d27b1a01366de0f5f93d007f480efc95b00263a391ba807aed88a3e421ab2d62b9e604456026434da79a0949a7b12f9c791ddf565ea9f107b4c1b8050d563ba5a633528f068dc30cf7a440cfe418de1f54c571ac670cc2a2fb17ee44e9d57f9c7cdadf0b7772bba6edb8a6b260631a01532da47bf3fc27fd5b137a4a449e0a25a4adb6b60eadd2ec7abec7e32f970fe50696932a324eb8a81297a23f6e27d082d3e7bb299625f591c887bcef5587fdfaad00d5e1faf2c1c17dd48b0504993f224285f3c40e8965bc39422455ff1e248a1ba8859079c361dd332b2246b733cf51cb21b38f704127a36cf08f92b9200a3f951b90979e061b5b3b3c7163fbb03eab9ec644a1f14b05c3e4d90ec541006d90c315a69a4bcbebba3bfb5a1be8814bf3006cdbfc7436673e967e1d2d0be00f12c2ec5b7fc3d05ae6d23cd318c77f9ba86f31dafd6c886a3bd0e05dbbaf6dd052c29e10c13355768314c14c1077f3e8ada461e49d3bb8e0d9270b25f1e35bc638e1c732188ff5a7e082cfd91183afdc79ecc78878e1e0122abf3b767051b936d82a5829000182e20381e802205236012662fbb977c807728310b40aebc827a02db9ee6a6eb618093047f3f416d82a5828000181e20392202003feb391e43dda2b8d156216efff3abad9143e71882e38ff2f78ed23db51e537000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2fdd661", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x352d011f8bc53595968e076db6f8bd657486a8fea38439ae699980096c2dc7e9", + "transactionPosition": 64 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d1d1d", + "to": "0xff000000000000000000000000000000002b1e7f", + "gas": "0x3735056", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000708181870819599dd82a5829000182e20381e80220d2d632c58c38d9ec51c905873d86022179e01c1508c4cc851f5f6a1ab3e4c4241a0037e0fc811a045b33ab1a004f64ead82a5828000181e2039220202f50056598e93a1acef58e7f51d3ea8bb0c7a50758cda43c77f43cd2eb09140100000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2660a8d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x905d04dc590112e6e1c646b036395ef60c40a0dd3d1692e5d7aa645b72206993", + "transactionPosition": 65 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b1e7f", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x367ea2f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x905d04dc590112e6e1c646b036395ef60c40a0dd3d1692e5d7aa645b72206993", + "transactionPosition": 65 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b1e7f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x35724e7", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x905d04dc590112e6e1c646b036395ef60c40a0dd3d1692e5d7aa645b72206993", + "transactionPosition": 65 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b1e7f", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x3450f76", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f64ea811a045b33ab0000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x478f65", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e2039220202f50056598e93a1acef58e7f51d3ea8bb0c7a50758cda43c77f43cd2eb091401000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x905d04dc590112e6e1c646b036395ef60c40a0dd3d1692e5d7aa645b72206993", + "transactionPosition": 65 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001db1ef", + "to": "0xff000000000000000000000000000000001db1f8", + "gas": "0x4ff0eb8", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a0001933e59078085a6a05ba075038c36680da91e35dc5e0342a4b0321542423416ef2cd460c0c4ff58ffa9ef1fa9cc25b2552a34c615dba6b9e7239402bf8e7c1d6c5c4310df78676f565d7a5cc7d86d9b5167dd1ab39f5ebae6bfdd2ce39f0121e4da37372620167a8403f8b0559db674be819fd9fd0fbf56c3d2380e69d7b24ac4ec5f15a984f2a347f7ba4369e2b4138a929933721085746f24dde157e3505c368b5f2da8f0f771be99312f7036e1104a8be9110ea577633f13ed70b3af39063c3421cbd673b3794651781e4b02612ac51ad3d045241c3f529ad25440301aea3e523b862ae800ae314e9a7d3c56c10df3febe2c2745a8431b0e400b9e78a151d50861417006dc356daa9dad6de15e4f62c1f3755e326fa69c95ef5bb94447912b0420eaf16b1034069ea566340dcaaad6b3c0e3230f0e24e829deba4880f6b14b069bc1fad638fda5443c98c7a23d733f50316b26ee887cda3dc12b890ebbfe8cd06f0a5e932937e994d3632086e26068050e0eb5e38788eb9917f96d995f9378d0705a71a682a4ea8657a5dc5b301e650f0ec27a5061b72f1b0378c5ccad2af6f73c802f87c56497ca82134b0b33ff48c211664128b2568daed030ab8a9395c7aa3bcd9e4f45570cfadcead43c36274ec16ceaf030b79cc110bb4526ebf51872b2420c025b0dd6eea34b9433bba6343fc6423ef42f6ff8ffd66e1f221d3385138a03155dad6d88e341d059a211606a7b23626046a5b11dcb4b1a36f783f863fe0d98a152c6a776498bb1ce5c9d36782f7ef973439585c50410ae820cdde1e5e78b7ed8f503b499e35f83958098c0d48896c0dc5e8fefe05a8cf42a28bec48f80dd2ac9de1fc09b409bb326b90a84b935c3867530b08df919bee3190d349e9416a8da77a23769d41f94990c342a042024389c6d6cd76994836fa3460ced9122476ec30848370c21e632ceef9ec9c06809562c90d008591f3005d92638efd1c3a4f6f8d86df6e0d39b30cf8f6ec91afc479db4e73016818d72c72e7ea7269eb2baa704b384e39e852103d92696cb10223e1262b3bcfb2f555ed19fbe9adc4cb16e50964be2f9aebcc0cc146466f72ec0c226e4d4e0d9846b99c646bfd5113884ae519ce08dfd6b08eb22a1af25b6d1815c7758bea333b9a410db11c75981e3c8fd36c663ee2fe62da024f01d684333317f1c8c075af0baff1dd9e1e647a295679900455cb16601e70b453351eda37402cfd61566ff38ae99f209e94a03976744a02b3a65f7b09d39bea0437cabc80d3971d0846d565d8bb1534add4e34f906b971460c23fd49f7ceb43db3647e8b4fc88417462477782a5dad0e67a4784c54490c5b21f971d192cb642a66c41d3c68a10b3655552b007fda2576a8cbb01ee3ff85021d26ddfd652a04942a586794ea3ba963ac24509ba0e8ee686f62f67c7d15d0efe99f86500f7f1c109939619a594b7ac7465179d1aebe00ea6a4fcb81ff9d3df670995d2202214db0a3d5eb371dc45fe23c3e82e7ec22f0d61b0d79714dcec70249fb447cbc96e50c387210bae5a4307d24dde932906bc825c16499f98c1837d6cf3561dd5fdb49618f6cfb5a1ca730566923a7836b769548ef077db369969121768624f1a7504590fe55a2dae2053b7b3f3f5301934abe9e804eddd301547c8d3aaa947caf2d21053d659786fbd959e2658777b9aa2808f3319a1189bad15b5f735bb6bc8e068acddb64e9131733ba193b38d9039f5646024ee3eeb5b952191dde3fd98019471737f6e6dc983d57a9fdee96aaf60598335fab83da3328b7f208d0731458e59377832573e74e6de7dc2337eb381fa37e4a08c79a65c754655d6af439d8ad54b347b98323dfe758e27178a40d63208fc1fcd3df61b2d7160757947d46c696a8843f7ce187c6656ac0008407a7b5f634405ea440bc3447ec91910599f4edd8def67045b4200778e4c9757c8e868bc08366fffedc3ce00dbff563b173552ae31d197a949f3d18bb143d460a0e02d3b4aaad2779c0820b2853477964deff42a9067033402aafefbd28dcae30cadf7e68a92b5499c47faf0a6cb25d3225dbbe1debb6a7f8be1692e1ed61ca929fa7b6d88a13905e6992dbf0e9a2102c50b8e346c9fa52e67516a226b4a6ed0a5175c2a4432c22882acb727a284191fb0207329a873381bdbfae72dc2a2f1e6be1c0a8bf77e090d056a7689b5b6763f128f8b76639404cc14d396d6604d2a8c5145ccb1bb72a3d899550b93b132cfff03f3b941c92677f1bc1d0d0bc9819ca0467643eaacd6eb7923124b15cd5cbefd24647152e11e3865c1e17bb1a35f7cd7a57d50998cd91f03efc0f5dc9b5658b8cc739f46745801fd9dedf3829a7ce44ce25ef3a0cb8b49b8c18c89247f4d5095c877d71302e66ccc2ac9debf3c6fb511bad40eae508965dfdf66ed8d45e218f8eb3fc13e98644b50ddd91dbc11d6944ec32801ce9761e585a69930468de3b262956f72e250eb0bb4711b0387509df05732feced968a04b24fe6805ecf93ea6d85d178f4af97ccc05811f56288f28972b3fb873a1b626056be4f4aabe5b3b8bcbfdcdc277e0854c1c3eace993ef36304bb9453cd91e76b60bfbca1bf1613426cdddabd05e689ba64cdbc8c4ffaf177e1a9731b38dca1fb28934b8140d54f6e0a636cdee02e18c31b79871bb61d4a1d171798460859b76e358af75eac0ea8c55d3dd93b584a0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7adabe", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x6393b0c1a03fcddab9d197626ee380c8dce03ac552781061a25ce03ed4a4b293", + "transactionPosition": 66 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001db1f8", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4b51aed", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a001db1f81a0001933e811a045b2300582097527d534b9bb0b5c38ed86fd4baf2c362b69fc2cb99f053439fd0d75ef7fa5e5820064b2b8946c028299181336eb551f58ccaa48f3f51029555218e5b6eb443910b59078085a6a05ba075038c36680da91e35dc5e0342a4b0321542423416ef2cd460c0c4ff58ffa9ef1fa9cc25b2552a34c615dba6b9e7239402bf8e7c1d6c5c4310df78676f565d7a5cc7d86d9b5167dd1ab39f5ebae6bfdd2ce39f0121e4da37372620167a8403f8b0559db674be819fd9fd0fbf56c3d2380e69d7b24ac4ec5f15a984f2a347f7ba4369e2b4138a929933721085746f24dde157e3505c368b5f2da8f0f771be99312f7036e1104a8be9110ea577633f13ed70b3af39063c3421cbd673b3794651781e4b02612ac51ad3d045241c3f529ad25440301aea3e523b862ae800ae314e9a7d3c56c10df3febe2c2745a8431b0e400b9e78a151d50861417006dc356daa9dad6de15e4f62c1f3755e326fa69c95ef5bb94447912b0420eaf16b1034069ea566340dcaaad6b3c0e3230f0e24e829deba4880f6b14b069bc1fad638fda5443c98c7a23d733f50316b26ee887cda3dc12b890ebbfe8cd06f0a5e932937e994d3632086e26068050e0eb5e38788eb9917f96d995f9378d0705a71a682a4ea8657a5dc5b301e650f0ec27a5061b72f1b0378c5ccad2af6f73c802f87c56497ca82134b0b33ff48c211664128b2568daed030ab8a9395c7aa3bcd9e4f45570cfadcead43c36274ec16ceaf030b79cc110bb4526ebf51872b2420c025b0dd6eea34b9433bba6343fc6423ef42f6ff8ffd66e1f221d3385138a03155dad6d88e341d059a211606a7b23626046a5b11dcb4b1a36f783f863fe0d98a152c6a776498bb1ce5c9d36782f7ef973439585c50410ae820cdde1e5e78b7ed8f503b499e35f83958098c0d48896c0dc5e8fefe05a8cf42a28bec48f80dd2ac9de1fc09b409bb326b90a84b935c3867530b08df919bee3190d349e9416a8da77a23769d41f94990c342a042024389c6d6cd76994836fa3460ced9122476ec30848370c21e632ceef9ec9c06809562c90d008591f3005d92638efd1c3a4f6f8d86df6e0d39b30cf8f6ec91afc479db4e73016818d72c72e7ea7269eb2baa704b384e39e852103d92696cb10223e1262b3bcfb2f555ed19fbe9adc4cb16e50964be2f9aebcc0cc146466f72ec0c226e4d4e0d9846b99c646bfd5113884ae519ce08dfd6b08eb22a1af25b6d1815c7758bea333b9a410db11c75981e3c8fd36c663ee2fe62da024f01d684333317f1c8c075af0baff1dd9e1e647a295679900455cb16601e70b453351eda37402cfd61566ff38ae99f209e94a03976744a02b3a65f7b09d39bea0437cabc80d3971d0846d565d8bb1534add4e34f906b971460c23fd49f7ceb43db3647e8b4fc88417462477782a5dad0e67a4784c54490c5b21f971d192cb642a66c41d3c68a10b3655552b007fda2576a8cbb01ee3ff85021d26ddfd652a04942a586794ea3ba963ac24509ba0e8ee686f62f67c7d15d0efe99f86500f7f1c109939619a594b7ac7465179d1aebe00ea6a4fcb81ff9d3df670995d2202214db0a3d5eb371dc45fe23c3e82e7ec22f0d61b0d79714dcec70249fb447cbc96e50c387210bae5a4307d24dde932906bc825c16499f98c1837d6cf3561dd5fdb49618f6cfb5a1ca730566923a7836b769548ef077db369969121768624f1a7504590fe55a2dae2053b7b3f3f5301934abe9e804eddd301547c8d3aaa947caf2d21053d659786fbd959e2658777b9aa2808f3319a1189bad15b5f735bb6bc8e068acddb64e9131733ba193b38d9039f5646024ee3eeb5b952191dde3fd98019471737f6e6dc983d57a9fdee96aaf60598335fab83da3328b7f208d0731458e59377832573e74e6de7dc2337eb381fa37e4a08c79a65c754655d6af439d8ad54b347b98323dfe758e27178a40d63208fc1fcd3df61b2d7160757947d46c696a8843f7ce187c6656ac0008407a7b5f634405ea440bc3447ec91910599f4edd8def67045b4200778e4c9757c8e868bc08366fffedc3ce00dbff563b173552ae31d197a949f3d18bb143d460a0e02d3b4aaad2779c0820b2853477964deff42a9067033402aafefbd28dcae30cadf7e68a92b5499c47faf0a6cb25d3225dbbe1debb6a7f8be1692e1ed61ca929fa7b6d88a13905e6992dbf0e9a2102c50b8e346c9fa52e67516a226b4a6ed0a5175c2a4432c22882acb727a284191fb0207329a873381bdbfae72dc2a2f1e6be1c0a8bf77e090d056a7689b5b6763f128f8b76639404cc14d396d6604d2a8c5145ccb1bb72a3d899550b93b132cfff03f3b941c92677f1bc1d0d0bc9819ca0467643eaacd6eb7923124b15cd5cbefd24647152e11e3865c1e17bb1a35f7cd7a57d50998cd91f03efc0f5dc9b5658b8cc739f46745801fd9dedf3829a7ce44ce25ef3a0cb8b49b8c18c89247f4d5095c877d71302e66ccc2ac9debf3c6fb511bad40eae508965dfdf66ed8d45e218f8eb3fc13e98644b50ddd91dbc11d6944ec32801ce9761e585a69930468de3b262956f72e250eb0bb4711b0387509df05732feced968a04b24fe6805ecf93ea6d85d178f4af97ccc05811f56288f28972b3fb873a1b626056be4f4aabe5b3b8bcbfdcdc277e0854c1c3eace993ef36304bb9453cd91e76b60bfbca1bf1613426cdddabd05e689ba64cdbc8c4ffaf177e1a9731b38dca1fb28934b8140d54f6e0a636cdee02e18c31b79871bb61d4a1d171798460859b76e358af75eac0ea8c55d3dd93b584ad82a5829000182e20381e802201c82068b925940ebe16ec679b86840d59d53a5afbc0ac8a51cf35e448f994a68d82a5828000181e203922020f96346a454a264d426a574e670033337c65e38a464a9fe95b5347d845167f73200000000000000000000000000" + }, + "result": { + "gasUsed": "0x3016322", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x6393b0c1a03fcddab9d197626ee380c8dce03ac552781061a25ce03ed4a4b293", + "transactionPosition": 66 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001db1ef", + "to": "0xff000000000000000000000000000000001db1f8", + "gas": "0x4c53635", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a0001936a590780979cc13480c6881871acf163c67f7a1a2bc1f03cc354f55dcf9dde15e48cc8a68e98e105f7e7616ad71c627e3d8f6e9b8943c90960eefd4a81a64f2fbdff01e1898df27da876dbda7c1e563a46fbfd254c52605b2add4b45236708ffa56c2fbc07908bb0d02575f826cdcb31c359c02aabad55583196cb94a22d4f17192f8b154a0e1a1f82153db370e15254e9f2976fadeb82b9f9dabed2109febad359bf7f04ea2468d908f72fd558a3e6f38c71e7e85bc1ba5111b542f05ccedd6e05b5599b348dcc5e5c24397b8bca23768f81ce95793fc3bc58d35ea8a77edcdf8dd3bb8bb527033e09f1f013bf3a4c14e050d4892ce22664802477bb67714e5ab3554f6f533905dcc74a467a55daad996dc9e7ba973238b0bc61cc5fc0dcf6f8867746b07a3026ffd5871c015495177899006daebe855e8b582b4141f55a94b06c07535589c5b736aef5faee110569bdebc43f5a97fb56e9378ada1f40298d28f98b3601e22b8029a62d1787652dada0e8b2fd1be40f8371bf9f2622ac367f91e3a4ff6a5d2b2c648eb2265eafc4e1c7a970ca09cfe7b3155bf10b42596bd5ff02f7d4e89010e9c4ac3a27aef7e8a543ca01310a89951f79a2f2429618dae9ae72baf2ebd4252816841e095b84d0d7abc399ef3d9435803749fa2bdf5ba8da6091e0e6c03b171affa5d8a3994ba6e269e2626318fa75808e6962ec2ce525b6ba5c3a395c90698aba9dd1f775527109dda8f3923a1b29dc929d02c9944f123f6b5a406b981ac7d94fdae623b963dabb44965a2ec39dbc3c350e060859f09b33c33b7183ca3507d467b152b9adefffb424b0411309bdb55882d0ed0284bd5891e81a2cf43243ea28da1815900beae3b1241952ff0989730da43746acef8bd5f5180291fdd707b206b7c5e3db17434cd1cf0c9951b147d9de68552128857e9867410bf03a50399230d371fcad03c528ddd8820305db233fb5f807f7268621f43b7bee3d44435ceb00b959a9298343ab73af240d2c6adc91fd617ee83aab7d65cf53248f30699745db31f4727a5a8f4137efc258d63621fd8326c90c26195e85686bea4c9eb8694df56f88732df7e7112b27259326c8d151410b922a23103b10ee6d81d325be256a6760c5b700fc4c21a99ae270f1fb4b6d0cc60948801c9d408bc6223eafcbe52dce0cc9f1ed2f2f2f52b3ba691418fda181a27417fbf753e14a8680b3ff4000579547b6c37c0d72e6d2e8b48280f96da26b838373d5a242342a9a46daefd8341852f2f53f5dfac1d8d40a3402ab891a0a8da280978d7803d88f4c52ab376d6db65c3cf315ee5f185e0f8b30d72f7da427654ead6a1790ff750bb41113313a32d9e81be3849240ec12417e7f16eca03a9a381b90352649b5ccfbd8a1ccaedaf858ef2cb1b31f85bce89d86e471984b30e029c13859ecc88301ade00414c5140b0a1ba262677a9d1f082287db7bdf4ab1f5d6ff313dbdbe62ac8faa283471019bcdb8670033166fc082d37812abe3a30284a67f7bf95c2ed53b1b521baa8d1069578cf5c01c8d09351fb27fe25a8d7b542582846c6b4c8c7f3decd48c0e21d6c00f6106e27a64e8b8a0cb6acf08c1d3fc566229e9317b81c372c53e0b4d61fb477b6c786b0bc58ee7a794b988fe8de466d93665535e19b98b909537d133aa4f4a995a89631da6007e4f698a0a2e529a0ece6534782c158195fa292657956c07d76c1668deb0ed453603c35a3bda1da85b5e29d766a17d87f1f995367f3801c066428d52d2c18a9dbb3934b69aa3c5cd6b20987df92d5cd1bbac10c805e33a9c6ec48162385eb4d2d9cd56dee21dc48ae2bb998b343be328590e079d8bf64b367c8169543df713d89ab398b9856c4c7437abd4ecb799cc0f5e56c7a165d1e8dababa0b8ee63474c0d31fa50e59274bdd73db1d0a6ba2fb590a523234fe5026bd276526293fe12ef6ead189f7d967a858a3243befb772036d5763730b6fd1d7d2515630653a23d63558ec0ef68428987bb3c6beb925703ca08c8333cc5aa742d0afc2a7fe498d6cc5f22f13d993a0b5023e6712c547c825a7b0adf77e9a88d93fc539e4f708e5c0fb9f6f44925ba5e2e97ebba350c59965d74652b17c3707440c8c7bb0604f4a0623a34425f37f8e03f50a8bfba1054ad50942a3f4e9c76486ca9a92219cbea8de506ca908a751ad9c51c1e9296f809b2b02f2f66a5659f5d8bd30426c484507723380cc1ab250482eda674e329f191abc9caa647f6ba3d54449e9969fc2a42f340b5440660da775488e5364d49de6a8a4fa8c81b836abc90ba13a01dedc03e28113d7f5d655b7071e665b9f13772632a5cc8df38593b76b6afe348878d07ef2cfb78c1210c26fbe21ea60564376f1b1770a732dd01a39af2f9ebd3b802686dc271b6046bcf3b1c0c8468506cfc654bb04aebe9d67d8814d0eeb9b1aab31c1f67ca73528ca48743992ac0ed70b4536b46ca73bfeb84e6678f5c730a05e6b02be30a8620830554179018980dff44d9bed0578b162191608205f506d2f427edd72d38e0a3b750aab0a7dc896a5cf8339f685ab8573e94728ced5a0a738844c3d8d64750c547383f99a227dd13138db4712808f367392979cd6227a3ce5930eeb8b2ec388b6fb7c0a2fd9da86ddbc94f7f71e87694781c69f428158a923c3c7b5199f7f9dd6d71eb50b8035b53e8bfeaa2ca0c7aaea9930c2df83b0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7ac08c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x49ef42aace56fbd0d5779635f363a2b37eb171aa85aefcf5218da7d495abf901", + "transactionPosition": 67 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001db1f8", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x47b5c9c", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a001db1f81a0001936a811a045b2a7758203e20b2105a6aae7da18e80f56751382eb6c4750b41c516ded263e3cd722d81f65820096c96c4cac9b18cb607419f842911557776091216b2c3a38014e9484ff0bb3a590780979cc13480c6881871acf163c67f7a1a2bc1f03cc354f55dcf9dde15e48cc8a68e98e105f7e7616ad71c627e3d8f6e9b8943c90960eefd4a81a64f2fbdff01e1898df27da876dbda7c1e563a46fbfd254c52605b2add4b45236708ffa56c2fbc07908bb0d02575f826cdcb31c359c02aabad55583196cb94a22d4f17192f8b154a0e1a1f82153db370e15254e9f2976fadeb82b9f9dabed2109febad359bf7f04ea2468d908f72fd558a3e6f38c71e7e85bc1ba5111b542f05ccedd6e05b5599b348dcc5e5c24397b8bca23768f81ce95793fc3bc58d35ea8a77edcdf8dd3bb8bb527033e09f1f013bf3a4c14e050d4892ce22664802477bb67714e5ab3554f6f533905dcc74a467a55daad996dc9e7ba973238b0bc61cc5fc0dcf6f8867746b07a3026ffd5871c015495177899006daebe855e8b582b4141f55a94b06c07535589c5b736aef5faee110569bdebc43f5a97fb56e9378ada1f40298d28f98b3601e22b8029a62d1787652dada0e8b2fd1be40f8371bf9f2622ac367f91e3a4ff6a5d2b2c648eb2265eafc4e1c7a970ca09cfe7b3155bf10b42596bd5ff02f7d4e89010e9c4ac3a27aef7e8a543ca01310a89951f79a2f2429618dae9ae72baf2ebd4252816841e095b84d0d7abc399ef3d9435803749fa2bdf5ba8da6091e0e6c03b171affa5d8a3994ba6e269e2626318fa75808e6962ec2ce525b6ba5c3a395c90698aba9dd1f775527109dda8f3923a1b29dc929d02c9944f123f6b5a406b981ac7d94fdae623b963dabb44965a2ec39dbc3c350e060859f09b33c33b7183ca3507d467b152b9adefffb424b0411309bdb55882d0ed0284bd5891e81a2cf43243ea28da1815900beae3b1241952ff0989730da43746acef8bd5f5180291fdd707b206b7c5e3db17434cd1cf0c9951b147d9de68552128857e9867410bf03a50399230d371fcad03c528ddd8820305db233fb5f807f7268621f43b7bee3d44435ceb00b959a9298343ab73af240d2c6adc91fd617ee83aab7d65cf53248f30699745db31f4727a5a8f4137efc258d63621fd8326c90c26195e85686bea4c9eb8694df56f88732df7e7112b27259326c8d151410b922a23103b10ee6d81d325be256a6760c5b700fc4c21a99ae270f1fb4b6d0cc60948801c9d408bc6223eafcbe52dce0cc9f1ed2f2f2f52b3ba691418fda181a27417fbf753e14a8680b3ff4000579547b6c37c0d72e6d2e8b48280f96da26b838373d5a242342a9a46daefd8341852f2f53f5dfac1d8d40a3402ab891a0a8da280978d7803d88f4c52ab376d6db65c3cf315ee5f185e0f8b30d72f7da427654ead6a1790ff750bb41113313a32d9e81be3849240ec12417e7f16eca03a9a381b90352649b5ccfbd8a1ccaedaf858ef2cb1b31f85bce89d86e471984b30e029c13859ecc88301ade00414c5140b0a1ba262677a9d1f082287db7bdf4ab1f5d6ff313dbdbe62ac8faa283471019bcdb8670033166fc082d37812abe3a30284a67f7bf95c2ed53b1b521baa8d1069578cf5c01c8d09351fb27fe25a8d7b542582846c6b4c8c7f3decd48c0e21d6c00f6106e27a64e8b8a0cb6acf08c1d3fc566229e9317b81c372c53e0b4d61fb477b6c786b0bc58ee7a794b988fe8de466d93665535e19b98b909537d133aa4f4a995a89631da6007e4f698a0a2e529a0ece6534782c158195fa292657956c07d76c1668deb0ed453603c35a3bda1da85b5e29d766a17d87f1f995367f3801c066428d52d2c18a9dbb3934b69aa3c5cd6b20987df92d5cd1bbac10c805e33a9c6ec48162385eb4d2d9cd56dee21dc48ae2bb998b343be328590e079d8bf64b367c8169543df713d89ab398b9856c4c7437abd4ecb799cc0f5e56c7a165d1e8dababa0b8ee63474c0d31fa50e59274bdd73db1d0a6ba2fb590a523234fe5026bd276526293fe12ef6ead189f7d967a858a3243befb772036d5763730b6fd1d7d2515630653a23d63558ec0ef68428987bb3c6beb925703ca08c8333cc5aa742d0afc2a7fe498d6cc5f22f13d993a0b5023e6712c547c825a7b0adf77e9a88d93fc539e4f708e5c0fb9f6f44925ba5e2e97ebba350c59965d74652b17c3707440c8c7bb0604f4a0623a34425f37f8e03f50a8bfba1054ad50942a3f4e9c76486ca9a92219cbea8de506ca908a751ad9c51c1e9296f809b2b02f2f66a5659f5d8bd30426c484507723380cc1ab250482eda674e329f191abc9caa647f6ba3d54449e9969fc2a42f340b5440660da775488e5364d49de6a8a4fa8c81b836abc90ba13a01dedc03e28113d7f5d655b7071e665b9f13772632a5cc8df38593b76b6afe348878d07ef2cfb78c1210c26fbe21ea60564376f1b1770a732dd01a39af2f9ebd3b802686dc271b6046bcf3b1c0c8468506cfc654bb04aebe9d67d8814d0eeb9b1aab31c1f67ca73528ca48743992ac0ed70b4536b46ca73bfeb84e6678f5c730a05e6b02be30a8620830554179018980dff44d9bed0578b162191608205f506d2f427edd72d38e0a3b750aab0a7dc896a5cf8339f685ab8573e94728ced5a0a738844c3d8d64750c547383f99a227dd13138db4712808f367392979cd6227a3ce5930eeb8b2ec388b6fb7c0a2fd9da86ddbc94f7f71e87694781c69f428158a923c3c7b5199f7f9dd6d71eb50b8035b53e8bfeaa2ca0c7aaea9930c2df83bd82a5829000182e20381e802209731f18aa4fce0cef187b30f84c05ce42eb42b7321ead72441898d5abdeea412d82a5828000181e20392202039f254d4d3235a7d50f05f37d5d73cdc32d12a9c989840c1108a54fb9a15523500000000000000000000000000" + }, + "result": { + "gasUsed": "0x3778f53", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x49ef42aace56fbd0d5779635f363a2b37eb171aa85aefcf5218da7d495abf901", + "transactionPosition": 67 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0e5", + "to": "0xff000000000000000000000000000000002cf0ea", + "gas": "0x338cc13", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708192d05d82a5829000182e20381e80220351469a2b481b840bd9743a6fcf7513753a3937e572c7cb25433031de7ec15651a0037e103811a045669031a004808a5d82a5828000181e20392202046998118b3148e63f207a911c0224a4eac12b07b6743415b6bff7524aac8023500000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2354b00", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0f12d60f9268de78ac12e64be7f99d6dd88c7d865d8ce91f32c11652baf62c16", + "transactionPosition": 68 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0ea", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x32d65ec", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0f12d60f9268de78ac12e64be7f99d6dd88c7d865d8ce91f32c11652baf62c16", + "transactionPosition": 68 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0ea", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x31ca0a4", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0f12d60f9268de78ac12e64be7f99d6dd88c7d865d8ce91f32c11652baf62c16", + "transactionPosition": 68 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cf0ea", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x30a8b33", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004808a5811a045669030000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x488993", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e20392202046998118b3148e63f207a911c0224a4eac12b07b6743415b6bff7524aac80235000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0f12d60f9268de78ac12e64be7f99d6dd88c7d865d8ce91f32c11652baf62c16", + "transactionPosition": 68 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce379", + "to": "0xff000000000000000000000000000000002ce3c0", + "gas": "0x305c21c", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708194375d82a5829000182e20381e8022019ea299b91070fce9a4ed47462469afe6593ac2a70357ce4d26758a7334278591a0037e112811a04548e851a00480c92d82a5828000181e203922020690da56aaad92166ebff80ea0daf666c3c7e1a59eb690b95367f47097cd4363c00000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x20c763a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x949c7c1e151376037350ef882355c9b80173632c806d5ca6164148b7f51ea7cf", + "transactionPosition": 69 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3c0", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x2fa5bf5", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x949c7c1e151376037350ef882355c9b80173632c806d5ca6164148b7f51ea7cf", + "transactionPosition": 69 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3c0", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x2e996ad", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x949c7c1e151376037350ef882355c9b80173632c806d5ca6164148b7f51ea7cf", + "transactionPosition": 69 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3c0", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2d7813c", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a00480c92811a04548e850000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x488993", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020690da56aaad92166ebff80ea0daf666c3c7e1a59eb690b95367f47097cd4363c000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x949c7c1e151376037350ef882355c9b80173632c806d5ca6164148b7f51ea7cf", + "transactionPosition": 69 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b3543", + "to": "0xff0000000000000000000000000000000021b478", + "gas": "0x1995446", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285068182004081820d58c08a3c01bd1c9be515f6217c646d9fec13e541dc1445178d0fee2bc33183b23dc4dc5b7d7d06493bb131a44566a5e1eecf8fb68474b5448d132996e4cc35db93b6e13b32f59f2d6a02eccc2f128271e171c09ce20cedfa3b7a57f6905d553af8e403305f072dadfa782b2e673feb8bebe6e75d8875a0af330a3ba0b404463b46ada804bde0677c9e44659a1973e82a243e88d1f18d823d6849eff98fab0e9c585246f8fc4a897916d823c91780159e530fd732352ad88e2474f2cf815c388dc3111a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1545d77", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x7d27a0315e51d96e06bc4699d5b5efcebf60f838a9db79f1c1ca6183ae3b86c5", + "transactionPosition": 70 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cbdce", + "to": "0xff000000000000000000000000000000002cbe59", + "gas": "0x4f47d1a", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782196c66590780906c8270f7a49125a567510c97404d726700444a974c52abaa369eacad17de494b817f70ae9f60f7e4b4fd7bb7325a17b8124630857a51652e94ee9a590f76dd479998cfaabbb74fc4dbdebe8be928083719ba2bc514dbf3723335072095ac5110206d71e43f6acb4c8d4a5f5c4489c2adfbacf8b54f44eeb958ccee349a7bd587c8ad64af6cbe885df7a04af2a4e1cda51388e3d459cb9efcc9905ffc53d4de4a7aed05a0537de2ce8594c672b5c3876bba195e095302b88d73f91b4d1716fab0eca1e729706fee665d539960ef9a4719ec4edc6bc0a4e6b52bb0e76ad484d5eeb2c1650ef9bc821d67524aa82f3bf4825548eb5d9b8a7f1638c747f219649c75241dbc9d1b47edf6baa29e7d85e667f9c96703e9ff09e3e5ee2be72a10a3e30f2e506c74bff2b581521fceb697c2a7b83db32f3c665e213c0eba5a8505d2415fb60ddebf509bbfd1a1dec3df01d480b23bdc16c8268bc0462d477101742b11bb5f31dfc037608610ad3489b78bd776e3e66e7655f5b55f98ef01edbe1c1ad0938b6883764bca24d33e2dae96c467ddb242b153f1bbb41df5d0842d496f1271e4cc9e212ab58369126cd56c30c602708f6b62ce8c1dd20b7eb5de15447c683536faee20399a2f23a619b2069546b279077a5b73c050962447c05997bb9c5831146230ac4f09cbb01eb7dc5abc87e185eb44da96695fd18f6047831d0276710e7aac3e850507ca142bbf4b036b72b872a3c351eff4e017b664d965c4c3a5b3745fb4dbd401f57ec16b0fcff536d8b705757f369e16ae1ea5b59e4f37d45b15d699257bdf5e01ea1be77e3dbb45a5204108e0009e93197e601b7da1d6bc29dab0c8126b99aef1ada965ea0a4c999b335c81095313b3d576be3ae66e4157a86a985285b64eff9380da6f2b4fcb5f6966ade1c97b4c293efc753316ad19d3e542600ef0539c6b1fa0ebed10edd8b6da5f276fc7d07075e1953120543573a4263641e9cf2c1e73df2d803339b29f7d9b14e08811c3f0d631c08f61853a59cffb9f585e2f409b1149602f4861bfafcbeabb4a8f6a0095e5b5cc20c3c7080e93563d4988839181355d52ec6c8e141890a662460b5d8e4b7a53f2abd3922221b848943b6aa79b10421431d8cf5e5973e7b28f218ab7578011d031c9f1ad86fba0f4915aa01e1dc34465c8e39caadf80d8e312e96b4e1ee885bafa1f2d0346461ea8939f08cd15cfa035611642aff82dede8f823d84aa267db9e9a439dddc9eb81ac4855a4746e844aa385b797c674f952b56ecc8a31180a1478fcb46486892bcd7f546b03546a6a2beb93c5412814a1ec8bf9c8cb8c098afbaac58bbfc45e1db9df070188c93ab52ec845d019ec1fc01f4a2e4efeb9930b71905de02d44736a3d07a7b7c34c4f4480dc8f8b974532afb2bb06ea8165a113710d5e429ba435c552fe7aa34183643a0e1b716447f71dc12ac4668f9c67a3f43195f9f4f9ee28451ac2445403c208be6a02cc5ac06012fd1209663aae19460d2afdff338e18f77a3cc12d9439ba513ddfe94c2f15754f39def5eec8b731642c48e08b4edb7c015578f02f69c4d8ba79b746a0caaee744715d94c139a6c9aa489a3ce3a343edac841181136c833f65a9dad3b1dca25f31f729fcb7dbebb47a11eaaf5a0f7904690b5b31a0bdcb88559d34ea252862b0a399dfcc57e283c0ea11412ab19438e439926f6dc9db6086c68ea329984dff59b636137eb83eb0a0c43b4e3d81c5e0e45bf305f0dc720d092086254dfc294477e5e5ff47c793feaa1866c94e251d5511229f1e95c05540b591984e73a1959b4094e62e71affd916daa99eabffc7ce1881e081be632ae2c09d61ad1b69e1b2bf064eae889182e86e0d65ec4667f75e2d277ee17b0cb85aabe7c6b7169011ed82f831758553ae49d4ea6e41b7e578c6e354c33fe528217a0f331ea1ba1350165b3098015cb58d38340811ff27e5e20a1368081fe140bd1ebc77dfdcdb28bc567e28b6584a7c68b68f6dec93e9cc385135038ee735322260f7c90044be8a19bf18d99e09f70607ff2a6a3a4ad29e0a291202eb601186b75e69835d2258e74bdbd04ec8f9c75981eb6f4dc7f09c01763aa3a3de91dd16c0b3f61b97802a4dd6dfccbc1da28fa09a5f8ee64b41ba12580d4a4163f84ec9f81a4edc5fba7fec105aeeb32dcb68d757cbd276c13a5e09af8302a11e5b503e7911d058ae7c1dcaa44b5927c9ab4f02aa796a478b2442b91f21877c46d91b41b07ba9b2b8514bd1adb787a9a3c27efa27fdeb8371e1d27c66ab80da9ec7c2c1f43145e73dd2483e3a93ea363c28131b8aadcbe8c7fd536caa63a79df8cd3a2e432fb449c6df33bc07a4a85328c5f0d7f1c89d5920bb7272f5b320df888a23877838b9bfb9a53217b09c71b5c5e0ccf18684ecfe43093a8586c2ca5567b99b52dde92398b19af9eaa852639e726d14c261610f64bb225b4d598b3e99f27fd427ccff640bf57173cd0ea640c4fd4662d9c14914d93457b202f3c020c4aec749ee965d824ce864b837797fdb365c5c20bfce3d5f3410308901bb2e6bbd2979af999c909d9f6b240a50559868ade0f8d1b22b8cf1fed7e084c000e3229b36b06c12f9eef090281bbc125f78415f6a9382b6646b5f3741ee2c39ca4a070e3fa322f138ae27f56f7bfecff510cd82b170b2d9c20bcf4133ab2cfb82278268f6a998e07cd00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x9f6264", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x09256a67c71f386327c50728db231ac787c819340a27da9c52bf5338efdc1b8c", + "transactionPosition": 71 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cbe59", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x485fdf9", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002cbe59196c66811a0458d8675820c733511a1ee67c4b6354648a0aad557a7d6103db91ba7ec3605d0e0d0a7dd727582070588192c2cfcb22bd6934b66eb4b782332859eca0353d7a6a6de5ae787e23ff590780906c8270f7a49125a567510c97404d726700444a974c52abaa369eacad17de494b817f70ae9f60f7e4b4fd7bb7325a17b8124630857a51652e94ee9a590f76dd479998cfaabbb74fc4dbdebe8be928083719ba2bc514dbf3723335072095ac5110206d71e43f6acb4c8d4a5f5c4489c2adfbacf8b54f44eeb958ccee349a7bd587c8ad64af6cbe885df7a04af2a4e1cda51388e3d459cb9efcc9905ffc53d4de4a7aed05a0537de2ce8594c672b5c3876bba195e095302b88d73f91b4d1716fab0eca1e729706fee665d539960ef9a4719ec4edc6bc0a4e6b52bb0e76ad484d5eeb2c1650ef9bc821d67524aa82f3bf4825548eb5d9b8a7f1638c747f219649c75241dbc9d1b47edf6baa29e7d85e667f9c96703e9ff09e3e5ee2be72a10a3e30f2e506c74bff2b581521fceb697c2a7b83db32f3c665e213c0eba5a8505d2415fb60ddebf509bbfd1a1dec3df01d480b23bdc16c8268bc0462d477101742b11bb5f31dfc037608610ad3489b78bd776e3e66e7655f5b55f98ef01edbe1c1ad0938b6883764bca24d33e2dae96c467ddb242b153f1bbb41df5d0842d496f1271e4cc9e212ab58369126cd56c30c602708f6b62ce8c1dd20b7eb5de15447c683536faee20399a2f23a619b2069546b279077a5b73c050962447c05997bb9c5831146230ac4f09cbb01eb7dc5abc87e185eb44da96695fd18f6047831d0276710e7aac3e850507ca142bbf4b036b72b872a3c351eff4e017b664d965c4c3a5b3745fb4dbd401f57ec16b0fcff536d8b705757f369e16ae1ea5b59e4f37d45b15d699257bdf5e01ea1be77e3dbb45a5204108e0009e93197e601b7da1d6bc29dab0c8126b99aef1ada965ea0a4c999b335c81095313b3d576be3ae66e4157a86a985285b64eff9380da6f2b4fcb5f6966ade1c97b4c293efc753316ad19d3e542600ef0539c6b1fa0ebed10edd8b6da5f276fc7d07075e1953120543573a4263641e9cf2c1e73df2d803339b29f7d9b14e08811c3f0d631c08f61853a59cffb9f585e2f409b1149602f4861bfafcbeabb4a8f6a0095e5b5cc20c3c7080e93563d4988839181355d52ec6c8e141890a662460b5d8e4b7a53f2abd3922221b848943b6aa79b10421431d8cf5e5973e7b28f218ab7578011d031c9f1ad86fba0f4915aa01e1dc34465c8e39caadf80d8e312e96b4e1ee885bafa1f2d0346461ea8939f08cd15cfa035611642aff82dede8f823d84aa267db9e9a439dddc9eb81ac4855a4746e844aa385b797c674f952b56ecc8a31180a1478fcb46486892bcd7f546b03546a6a2beb93c5412814a1ec8bf9c8cb8c098afbaac58bbfc45e1db9df070188c93ab52ec845d019ec1fc01f4a2e4efeb9930b71905de02d44736a3d07a7b7c34c4f4480dc8f8b974532afb2bb06ea8165a113710d5e429ba435c552fe7aa34183643a0e1b716447f71dc12ac4668f9c67a3f43195f9f4f9ee28451ac2445403c208be6a02cc5ac06012fd1209663aae19460d2afdff338e18f77a3cc12d9439ba513ddfe94c2f15754f39def5eec8b731642c48e08b4edb7c015578f02f69c4d8ba79b746a0caaee744715d94c139a6c9aa489a3ce3a343edac841181136c833f65a9dad3b1dca25f31f729fcb7dbebb47a11eaaf5a0f7904690b5b31a0bdcb88559d34ea252862b0a399dfcc57e283c0ea11412ab19438e439926f6dc9db6086c68ea329984dff59b636137eb83eb0a0c43b4e3d81c5e0e45bf305f0dc720d092086254dfc294477e5e5ff47c793feaa1866c94e251d5511229f1e95c05540b591984e73a1959b4094e62e71affd916daa99eabffc7ce1881e081be632ae2c09d61ad1b69e1b2bf064eae889182e86e0d65ec4667f75e2d277ee17b0cb85aabe7c6b7169011ed82f831758553ae49d4ea6e41b7e578c6e354c33fe528217a0f331ea1ba1350165b3098015cb58d38340811ff27e5e20a1368081fe140bd1ebc77dfdcdb28bc567e28b6584a7c68b68f6dec93e9cc385135038ee735322260f7c90044be8a19bf18d99e09f70607ff2a6a3a4ad29e0a291202eb601186b75e69835d2258e74bdbd04ec8f9c75981eb6f4dc7f09c01763aa3a3de91dd16c0b3f61b97802a4dd6dfccbc1da28fa09a5f8ee64b41ba12580d4a4163f84ec9f81a4edc5fba7fec105aeeb32dcb68d757cbd276c13a5e09af8302a11e5b503e7911d058ae7c1dcaa44b5927c9ab4f02aa796a478b2442b91f21877c46d91b41b07ba9b2b8514bd1adb787a9a3c27efa27fdeb8371e1d27c66ab80da9ec7c2c1f43145e73dd2483e3a93ea363c28131b8aadcbe8c7fd536caa63a79df8cd3a2e432fb449c6df33bc07a4a85328c5f0d7f1c89d5920bb7272f5b320df888a23877838b9bfb9a53217b09c71b5c5e0ccf18684ecfe43093a8586c2ca5567b99b52dde92398b19af9eaa852639e726d14c261610f64bb225b4d598b3e99f27fd427ccff640bf57173cd0ea640c4fd4662d9c14914d93457b202f3c020c4aec749ee965d824ce864b837797fdb365c5c20bfce3d5f3410308901bb2e6bbd2979af999c909d9f6b240a50559868ade0f8d1b22b8cf1fed7e084c000e3229b36b06c12f9eef090281bbc125f78415f6a9382b6646b5f3741ee2c39ca4a070e3fa322f138ae27f56f7bfecff510cd82b170b2d9c20bcf4133ab2cfb82278268f6a998e07cdd82a5829000182e20381e80220650c58df573197bd182a131c412403032df10dd4be914cad3fee6a8f9f50490dd82a5828000181e203922020bbfd25d9bb672844a5597ab0c099ddd592766e9cc6bfbc1aabcbdeb78b5af603000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x378821c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x09256a67c71f386327c50728db231ac787c819340a27da9c52bf5338efdc1b8c", + "transactionPosition": 71 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cbdce", + "to": "0xff000000000000000000000000000000002cbe59", + "gas": "0x5842604", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782196cb759078088616c745421a5e0810320c2d4127fb35257616beecae3a3857f10eef0cbe560c70555cf106e7df9ff45475bd7e7f097ada94a97f56afacb8367bae0a7c9b9e07bd27f4372024646cb53abdc2f6be656423de773c1a708381a3424184cf315e80ea091cecc1005df11c33d569f0d275d97d25892f931e33d3817f556f964d44bf425022a7d06d67442c2ad9304f5e6b9b42497880067eaf6df1d5b6d405f1a7296142ea97427040300ece471973832fa6b0dc8cfbd4dbb4b747475ac001497f980a801e5d9ed67095e8253cd38643181f78ed6d1fd39f9d5969faa493b5136c40536e7f464cf02d8b67b682e34825f8d9755f05de55886dc90e1058deab0272e02ecfbbca1f12e12e56123cc2c5ea67f8631520d5691b5a78ddfe1af7e40bebd163fe9a61c4f8a9d02a3939bd06bc3665a97fc75ba9bb0febcfff1b0339d6a88df855e5a15c9a6693164bd5d3e9220869867de3399f97ab90ae31232176481decab82cae43a9c82944ce462cc38e68eb7a112acd831286494b5842f2afd857dda77c58d2134652c533d730e6b5ae06f7d3437c4489c590215f5c8371b355ff2d934d166a48a586e02e8e7f48730810d4b591b5cfad7ebdb03553a70af3c23a214b7b06392f902cbbc6766ca703c25e9122e46cf6b698c6b7b615f8cc1cae3a3f1253f5fadca7be1d9548af2ea031ee8aea3cea3be303178c1d01c9b3b2d5e99daf1546921fb56aaeda89be4f70a11343a4c6a8459454783024ab4aeabba105a969b453cb83ad6dd19bbd8bee6dfe04de7bac58fb49abc7ab6f63831a904a0b66b209f3326c4e21854aff6c2be4f393a145290a9574ae7ab0d6c90ab17da9c5e1c027f06ad6c19797e8f411f5499a6c62b1d8cfa70b6f61fd3f6100b93d6b53141ac5598568e9357449ee5c2b1d444ea251a41956b52d35e2b7cd26dc33c3eaa5067ff1da021e9901f02fa9e7a5a5a55b67c336655a31407ba2899156fc7dc35f221422cdf332c3fd52978b644c6e73a2aff102ff518f8d3ecacdbc64cc07a70f4d62fd677c740f3470e41bf0e28288f3bf8c615054fd2a853c0f80fd284b979999db697097029be9b7a5a0ef6c8abc89ac176180a208cd524679f10c6a9403472f9ff960cebec3e33b9912d62f455ef0adb936e5f88e24127e4f4dd46fe7ff9ae1317c217a630a0760bc84ff5fba0b48b047e5eb25975e00572c6b6dbe561ade01ea50f345d95c91c866537d86c0720e7396dff985a191fd554f838289ae148fb45a6db5ebb752f2690ffcb42e75c29aa74b3f566789ad96d7eda31320391c160dbe295d4a8102d4b331d8a1e1290e41bff958c5e0155f333e1846b21d1a14e0871bacecd05b4a16370aacf1319016359ac2d1d9d3911dd41b1936cc9f0d8d0a33196d4805e869e801df13e1046487c5a0fe53944aa55a6737cc6e0a84fce49f514d76c68e9be3c782c64b788d92c6c04f4c158d5c63670ca19e1a85024017d2160f591c8ac226ffdf96733ccb6bda368b266f6b326ff8b01227d1f4fcbe2850a57c9b71ea234817a198d111f8f81b59b12a4bc48614ec5364db7ef0205315c3530e418f5a039e05883b1401c9d9de917e25a1324bdd5f693cb93a8bd86db1d797bd6be5f9a4078c6710690da48a2b5058939427c91b9184bfba9199a5ce398f3e851e0b2299a1138e3ac216f64a9d8981f312c74419106595e549d9dfcbe7bd6f803995f9f36d20685e0c771c6b73e73c602bb0e18c7c1b71a2b18cababbd1a1891bb3b4b927e58bc8a7b5d6c3433640985609b3e3baf4c178f3a469a52a2697a185fa6b57c2a6d6371cf02f18cdba2ae3b7467a3663962e333726df013118dd4e5f91b258df39a3870e252712e87cc44f2e9e3a7b6e4c503088efa5ce363e7b10d2ce33a87a85629482cb5d64c01ec4f97d0b7f704a8fd30590bcefa2b3699a133a39e0a383cf5db3fcaa299b0154eb3dccdecbd83c3daf38216cd7ce9c9b436ef33decba8a32a2f1a64e5539ebe5b5432d5a438a24d2d6e8ca07f33519cc601dfe0551faeb8514b005c79ad7ed2c7a1f8c9cc2804ca4170bd1ab04e00c0d07e5bef9895515ad0485dcc4b00cdba458655f3818dcdf7757ba76fff722b9856c3925ff17e0213075adcccd677fc0794c92930da09419fbe0fc78c98d466c550a6541aa4e5a01689d9634d384d8b7f77e678676555b342d1f54ab3f7719191c4bc98577a0ead9b05b86e16bd1bdc018a98f467472d8b8e54ca29642b2020d11595a282b3cabd7689bc79d0c5822fa6d3b539a962bdd17b3eb3adf7c34ca9eb2c00a7e18d11e5290d11878e2437876e4d306e2fab457fab2c52deccf7219830e2308d87441b33906d7334feab7c6e9259b8136502f237344b954ab861132dc40769b10ac6f4e4933fa08422e2f499a7e588caec50bb4912e85df69e97c6a148a6961d683b631c82a5e96080368d6800c9908f5e36f93d2d4b3ebcbdeff3e492b26faec713df3f5e74536a313effcb12e1b0c3d2c3e290d34ec267b895f23ca7511cdc0be7dfb6648163f8731e9833b73b77697ee24bb2012cf36986470798b1d00fa384d2e0eeb172ea82d705c6b55785c0c1fc64b0f85081dea8f9b9242961e1251f0d3bc474b6db1977efd1c0d217fbaa15682c62fd9d386fe37b710fb7a3e86018b629cf0c15cb32509633f555f7750c4f553798b97b008b8347c3dedc9d4900000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xa41eb9", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xfe2a899422ce49a8aa92413b3b30dfeef07f4601e1f817a9be1e2681e0c6a3c5", + "transactionPosition": 72 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cbe59", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x510df01", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002cbe59196cb7811a0458d1025820bdda037c22b1041a1ab65a83f366c6dda203b1a83f8a11f7e6d76b019411a960582070588192c2cfcb22bd6934b66eb4b782332859eca0353d7a6a6de5ae787e23ff59078088616c745421a5e0810320c2d4127fb35257616beecae3a3857f10eef0cbe560c70555cf106e7df9ff45475bd7e7f097ada94a97f56afacb8367bae0a7c9b9e07bd27f4372024646cb53abdc2f6be656423de773c1a708381a3424184cf315e80ea091cecc1005df11c33d569f0d275d97d25892f931e33d3817f556f964d44bf425022a7d06d67442c2ad9304f5e6b9b42497880067eaf6df1d5b6d405f1a7296142ea97427040300ece471973832fa6b0dc8cfbd4dbb4b747475ac001497f980a801e5d9ed67095e8253cd38643181f78ed6d1fd39f9d5969faa493b5136c40536e7f464cf02d8b67b682e34825f8d9755f05de55886dc90e1058deab0272e02ecfbbca1f12e12e56123cc2c5ea67f8631520d5691b5a78ddfe1af7e40bebd163fe9a61c4f8a9d02a3939bd06bc3665a97fc75ba9bb0febcfff1b0339d6a88df855e5a15c9a6693164bd5d3e9220869867de3399f97ab90ae31232176481decab82cae43a9c82944ce462cc38e68eb7a112acd831286494b5842f2afd857dda77c58d2134652c533d730e6b5ae06f7d3437c4489c590215f5c8371b355ff2d934d166a48a586e02e8e7f48730810d4b591b5cfad7ebdb03553a70af3c23a214b7b06392f902cbbc6766ca703c25e9122e46cf6b698c6b7b615f8cc1cae3a3f1253f5fadca7be1d9548af2ea031ee8aea3cea3be303178c1d01c9b3b2d5e99daf1546921fb56aaeda89be4f70a11343a4c6a8459454783024ab4aeabba105a969b453cb83ad6dd19bbd8bee6dfe04de7bac58fb49abc7ab6f63831a904a0b66b209f3326c4e21854aff6c2be4f393a145290a9574ae7ab0d6c90ab17da9c5e1c027f06ad6c19797e8f411f5499a6c62b1d8cfa70b6f61fd3f6100b93d6b53141ac5598568e9357449ee5c2b1d444ea251a41956b52d35e2b7cd26dc33c3eaa5067ff1da021e9901f02fa9e7a5a5a55b67c336655a31407ba2899156fc7dc35f221422cdf332c3fd52978b644c6e73a2aff102ff518f8d3ecacdbc64cc07a70f4d62fd677c740f3470e41bf0e28288f3bf8c615054fd2a853c0f80fd284b979999db697097029be9b7a5a0ef6c8abc89ac176180a208cd524679f10c6a9403472f9ff960cebec3e33b9912d62f455ef0adb936e5f88e24127e4f4dd46fe7ff9ae1317c217a630a0760bc84ff5fba0b48b047e5eb25975e00572c6b6dbe561ade01ea50f345d95c91c866537d86c0720e7396dff985a191fd554f838289ae148fb45a6db5ebb752f2690ffcb42e75c29aa74b3f566789ad96d7eda31320391c160dbe295d4a8102d4b331d8a1e1290e41bff958c5e0155f333e1846b21d1a14e0871bacecd05b4a16370aacf1319016359ac2d1d9d3911dd41b1936cc9f0d8d0a33196d4805e869e801df13e1046487c5a0fe53944aa55a6737cc6e0a84fce49f514d76c68e9be3c782c64b788d92c6c04f4c158d5c63670ca19e1a85024017d2160f591c8ac226ffdf96733ccb6bda368b266f6b326ff8b01227d1f4fcbe2850a57c9b71ea234817a198d111f8f81b59b12a4bc48614ec5364db7ef0205315c3530e418f5a039e05883b1401c9d9de917e25a1324bdd5f693cb93a8bd86db1d797bd6be5f9a4078c6710690da48a2b5058939427c91b9184bfba9199a5ce398f3e851e0b2299a1138e3ac216f64a9d8981f312c74419106595e549d9dfcbe7bd6f803995f9f36d20685e0c771c6b73e73c602bb0e18c7c1b71a2b18cababbd1a1891bb3b4b927e58bc8a7b5d6c3433640985609b3e3baf4c178f3a469a52a2697a185fa6b57c2a6d6371cf02f18cdba2ae3b7467a3663962e333726df013118dd4e5f91b258df39a3870e252712e87cc44f2e9e3a7b6e4c503088efa5ce363e7b10d2ce33a87a85629482cb5d64c01ec4f97d0b7f704a8fd30590bcefa2b3699a133a39e0a383cf5db3fcaa299b0154eb3dccdecbd83c3daf38216cd7ce9c9b436ef33decba8a32a2f1a64e5539ebe5b5432d5a438a24d2d6e8ca07f33519cc601dfe0551faeb8514b005c79ad7ed2c7a1f8c9cc2804ca4170bd1ab04e00c0d07e5bef9895515ad0485dcc4b00cdba458655f3818dcdf7757ba76fff722b9856c3925ff17e0213075adcccd677fc0794c92930da09419fbe0fc78c98d466c550a6541aa4e5a01689d9634d384d8b7f77e678676555b342d1f54ab3f7719191c4bc98577a0ead9b05b86e16bd1bdc018a98f467472d8b8e54ca29642b2020d11595a282b3cabd7689bc79d0c5822fa6d3b539a962bdd17b3eb3adf7c34ca9eb2c00a7e18d11e5290d11878e2437876e4d306e2fab457fab2c52deccf7219830e2308d87441b33906d7334feab7c6e9259b8136502f237344b954ab861132dc40769b10ac6f4e4933fa08422e2f499a7e588caec50bb4912e85df69e97c6a148a6961d683b631c82a5e96080368d6800c9908f5e36f93d2d4b3ebcbdeff3e492b26faec713df3f5e74536a313effcb12e1b0c3d2c3e290d34ec267b895f23ca7511cdc0be7dfb6648163f8731e9833b73b77697ee24bb2012cf36986470798b1d00fa384d2e0eeb172ea82d705c6b55785c0c1fc64b0f85081dea8f9b9242961e1251f0d3bc474b6db1977efd1c0d217fbaa15682c62fd9d386fe37b710fb7a3e86018b629cf0c15cb32509633f555f7750c4f553798b97b008b8347c3dedc9d49d82a5829000182e20381e80220ad21a8b5617018e5872ad54d2fb83e094fb5178f7ac7530bc2a82c91c9b0e473d82a5828000181e203922020bc3b85c5479f349cc048347aa46fcd4a1364eb6ec3b191d86bfd70ae490cdc32000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3e69aeb", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xfe2a899422ce49a8aa92413b3b30dfeef07f4601e1f817a9be1e2681e0c6a3c5", + "transactionPosition": 72 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000181d8a", + "to": "0xff00000000000000000000000000000000181e6b", + "gas": "0x1959fb3", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f385181a8182004081820e58c0a829718db09f68797f2d9be15809d2e1387a94bd8b1a51d53013dab9b9611d70aece45b4771bd878d93db7f151e76e9a812d513d34dd30afd572af6adb1763bf9583245961b2e14558086e2eb0da8a1fbf572d042dc1f0d6f0ac2bdab7ed340306aa8db0e432b57aca8579449eab439fc87f84c441133383e504824927027f70f722acfaebc2ebe1ab004f5989794a51b59d9f1ac7749ae9d4cf9b0860b9b1f8b9bd11b0e7f183bf20e9d1ad2278791c6c438c3c768c6e9395786a463bd9ba4f1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000000000" + }, + "result": { + "gasUsed": "0x1517eb4", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1b93e4163b1bd35cb6342233c84dcda34d3347e5f5ea996f90e63b2d0e71f98e", + "transactionPosition": 73 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2abc", + "to": "0xff000000000000000000000000000000002c2ade", + "gas": "0x4dea6fd", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000007878219911e590780960f552b98a30ad3840c6da73d540213f5723c320b231419709ba5b7381feff81e38de449fdf783e55f3f411c1baa728854228f264cb115325348d007cb5922c59e926caf1a0d78fe1b4ec9841883cfd34d7a6b2eaf574097ca32f079d4a862a12245f8aec0237e66ee5b29da0ab67f6d5da06f5311dc40f205aef2c09826d84283d6c951d802f3a4dbba31b38abf15fa07625c908e2c2797578c1431ee2894e7d954bf134439530ae612ebcf87deb1f354bc3bb96bfa4f4ca53c79112772671891803ee5c6dc2bebf2beea9bed50c947b731c5faf9369b3c42fb5cfe9d0236f4f559a05a7a8a584cf26637e6713ad1a80520fb7f0d3e6f3c70866b6371c17ee1ae76dbbcc809a0c2a70a2e6241ed2b04d9f35f5e9d215e346266321856099c81550a8d7b7e6a82bb31f5002c5545e9e853ae79cbe26d5d9fbdca504de06b48dbec1614875ead88b1aacc0a76a6e1123b91092b53b92d6507d2c4b90b950406e3706e22fd16ba83ef4474f93edf8e908ca824aa875175425b757365e0d94af4d8f054278185210db77b78c66d44ead9ad621b2080b8a96d41aece81e05638984440954d538f31a8d90847550cfc56846a51c83ec9630241066785b926985a90ab0d30f815a1a365adadf5560b911a33336e278dcd77eceef98eaaa2acc96c03806004221de8e1e43c097d2ddd72870cf493361094483976e1cf852362c17559004265230b9f6582e572b3f359a2242948c49b4273efce8cf7e2ae233ab96db76f22b2214c2c2b58edc3faa2fc67e589bad0a2eb05e0830be889f439d27a7c1dab781d301ac88790943ade6fd61679b08f70bfdde55c4f98f8d41a9ff2ad730a8221eacb29d4b26ddcf813bffa913bf5ab88e3f73a9a3b6b8b8e433ecb6ba009384e088519a4422bb250aacf4cc81d39be173eb79b52b2991171310067a240b6507a0b344cecd6896062ed7dc28022567c5236f2ad5e97fbd6c37621fbfe727b11536ee5e6ba31030f252ce8a05224bc69617ba5aa7a5a8561d68c97b7532181c77df9804feb4853fdd3efbff953eb9d8e826ea01b27919b33911bbec2f9041b2aa05c84419df21b1f91515ec4467a3abcb30aab03f04018b351ec6684a1fb67e7f67f356ac375c34f2a181d718318f92b97acd7328907279031a4b93eebc88b4fc70b4e831be82deef3e724b3af5608f1f311723a3a3c7dfa2be97407358469e04304456ee864fc2b212eaa3adff6d89c1991ca2fa1e988eb2769178e935a996ac0c1efa44000328ff4667474f79536499392534a06cd4638cfabbddaab5028f202a3c7364fc01f147902c9bdbc2bfd750498927e9a0769330daada7cb605619ad35a6f016f2405e9978b6df94c5226f1c796328027b9be8f87dfd536ffd4a3cba032084902817ef3b5a014a29d81db98cf0f0ffcc696a4408a40097d0e809237cab607f0ec005572c41cc3a7829a2ecbd9df4280d283f23882fbd6d159e208f0447ef60c9c20ed40115ca2631ebac7a8b5920dfeb64c5354fc1a6a3e489b33e9fe5c7dbf8139d06ed75b648aeaa7995b013265228524981c8f09cc1268bb0a626ab89d3d829fefafddd2a7734787f1e691f862ded29aec3478520af69fda96493b04ba75b38ed974f0ed859bf8703f7b9579c2bc9358994a71481b1450f791996ddd13b9faee4684551059290a96485a537d58087d1e857eb09bf9e2896542631756c5bab31ee5c4a2b4bee767d62945ae42aed7edb1d294df7698b64f1b603179a12f1efd28980a98ba42890c2afcafebf1a970f660ab38ec4120192e3087a438c1860274a65e5928726c4e42bfd07a8adf53878b8aef598229d957591aa63b0e9e3e21a1ad7df441c5292a24f4e6f47e6dd12310a9037da22169cdcb1e46fb20f63afc4133ccb82a4ec7e21af6670e565090ab805d9a93b30648b57e472f24deb499426392b6134b2de005119751aadbdb7535e60042f0f8d549522bacdaa1c55572d267294fd3f2c98230004b05d2f2b2315acf1e4e9d46d71e3811b25690382e88dedf7046af4d4439aaedc1e8bdbed282aeed7d88e451e393bcabb3e8c7e6343aceddaf51dceaaa4041231b5008bdee7e20d551bc2003d54518a240afc80bee8c709256971ce9ae1ed882c2f080ffcc7e9cf0b5a15a678e81ed782fe8ab12df8aac05305bac6864eeb2112012703cc57fa1c7c09845cab94dad1eb3562aeeb75dee6b56fb23f915865f0fa577bae17fbba3d8dab489dc750e20d4bade21b94fa009563993932a5f67cadd5904177a457ede06a0bb3e41f64af699d4b2e0f11368d7c9bd7a490a34b334766df92964dec91fd5cc7e1768319c3b761a16ce91b980ec58ae990b2d2828a0e780a8e84967fa7c03cc052d842cdc645b47f3b000286cc8f28fee0542bd0a715c04816572412a78e07a595f646354de4e8959a80713401b9bae9624f6fd9828d1f36c440417ab6c6e4f7d3ce8f165529983ab31f1bf09eb67f7b19d6f192548cdb5f34accfdfd6a69f081cf9ffa9994cb5824ff1f89bd9915ceb08eb935c7d1181aba7da21f540b7351098fcfb708e91d65d7b091cb0b17dd60f241e87cd8e6635b0eba3b015185d25de049b63a7c32ce752989dac83f03a2af6664f6eb072202100a5a6d10a8ea84d2cc10872acafd6e2989c32baf41e3c4f3d5d55ec352f30517ebd74b96a10b80454e52009898eab5a699900000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x96b2d7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf14f04259ac556d1381965bd9d12c803e7e1ec83656ef48e3d57130963a8fd10", + "transactionPosition": 74 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c2ade", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x478d47b", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002c2ade19911e811a0459c3f65820be2d62bd4039006ec2601e9b85f158e3facb09f64592cf07d68ade65df8ce7345820a821855a854abe1c7ada3b128358010c99e1da5bea3dd573733e8978edfe3483590780960f552b98a30ad3840c6da73d540213f5723c320b231419709ba5b7381feff81e38de449fdf783e55f3f411c1baa728854228f264cb115325348d007cb5922c59e926caf1a0d78fe1b4ec9841883cfd34d7a6b2eaf574097ca32f079d4a862a12245f8aec0237e66ee5b29da0ab67f6d5da06f5311dc40f205aef2c09826d84283d6c951d802f3a4dbba31b38abf15fa07625c908e2c2797578c1431ee2894e7d954bf134439530ae612ebcf87deb1f354bc3bb96bfa4f4ca53c79112772671891803ee5c6dc2bebf2beea9bed50c947b731c5faf9369b3c42fb5cfe9d0236f4f559a05a7a8a584cf26637e6713ad1a80520fb7f0d3e6f3c70866b6371c17ee1ae76dbbcc809a0c2a70a2e6241ed2b04d9f35f5e9d215e346266321856099c81550a8d7b7e6a82bb31f5002c5545e9e853ae79cbe26d5d9fbdca504de06b48dbec1614875ead88b1aacc0a76a6e1123b91092b53b92d6507d2c4b90b950406e3706e22fd16ba83ef4474f93edf8e908ca824aa875175425b757365e0d94af4d8f054278185210db77b78c66d44ead9ad621b2080b8a96d41aece81e05638984440954d538f31a8d90847550cfc56846a51c83ec9630241066785b926985a90ab0d30f815a1a365adadf5560b911a33336e278dcd77eceef98eaaa2acc96c03806004221de8e1e43c097d2ddd72870cf493361094483976e1cf852362c17559004265230b9f6582e572b3f359a2242948c49b4273efce8cf7e2ae233ab96db76f22b2214c2c2b58edc3faa2fc67e589bad0a2eb05e0830be889f439d27a7c1dab781d301ac88790943ade6fd61679b08f70bfdde55c4f98f8d41a9ff2ad730a8221eacb29d4b26ddcf813bffa913bf5ab88e3f73a9a3b6b8b8e433ecb6ba009384e088519a4422bb250aacf4cc81d39be173eb79b52b2991171310067a240b6507a0b344cecd6896062ed7dc28022567c5236f2ad5e97fbd6c37621fbfe727b11536ee5e6ba31030f252ce8a05224bc69617ba5aa7a5a8561d68c97b7532181c77df9804feb4853fdd3efbff953eb9d8e826ea01b27919b33911bbec2f9041b2aa05c84419df21b1f91515ec4467a3abcb30aab03f04018b351ec6684a1fb67e7f67f356ac375c34f2a181d718318f92b97acd7328907279031a4b93eebc88b4fc70b4e831be82deef3e724b3af5608f1f311723a3a3c7dfa2be97407358469e04304456ee864fc2b212eaa3adff6d89c1991ca2fa1e988eb2769178e935a996ac0c1efa44000328ff4667474f79536499392534a06cd4638cfabbddaab5028f202a3c7364fc01f147902c9bdbc2bfd750498927e9a0769330daada7cb605619ad35a6f016f2405e9978b6df94c5226f1c796328027b9be8f87dfd536ffd4a3cba032084902817ef3b5a014a29d81db98cf0f0ffcc696a4408a40097d0e809237cab607f0ec005572c41cc3a7829a2ecbd9df4280d283f23882fbd6d159e208f0447ef60c9c20ed40115ca2631ebac7a8b5920dfeb64c5354fc1a6a3e489b33e9fe5c7dbf8139d06ed75b648aeaa7995b013265228524981c8f09cc1268bb0a626ab89d3d829fefafddd2a7734787f1e691f862ded29aec3478520af69fda96493b04ba75b38ed974f0ed859bf8703f7b9579c2bc9358994a71481b1450f791996ddd13b9faee4684551059290a96485a537d58087d1e857eb09bf9e2896542631756c5bab31ee5c4a2b4bee767d62945ae42aed7edb1d294df7698b64f1b603179a12f1efd28980a98ba42890c2afcafebf1a970f660ab38ec4120192e3087a438c1860274a65e5928726c4e42bfd07a8adf53878b8aef598229d957591aa63b0e9e3e21a1ad7df441c5292a24f4e6f47e6dd12310a9037da22169cdcb1e46fb20f63afc4133ccb82a4ec7e21af6670e565090ab805d9a93b30648b57e472f24deb499426392b6134b2de005119751aadbdb7535e60042f0f8d549522bacdaa1c55572d267294fd3f2c98230004b05d2f2b2315acf1e4e9d46d71e3811b25690382e88dedf7046af4d4439aaedc1e8bdbed282aeed7d88e451e393bcabb3e8c7e6343aceddaf51dceaaa4041231b5008bdee7e20d551bc2003d54518a240afc80bee8c709256971ce9ae1ed882c2f080ffcc7e9cf0b5a15a678e81ed782fe8ab12df8aac05305bac6864eeb2112012703cc57fa1c7c09845cab94dad1eb3562aeeb75dee6b56fb23f915865f0fa577bae17fbba3d8dab489dc750e20d4bade21b94fa009563993932a5f67cadd5904177a457ede06a0bb3e41f64af699d4b2e0f11368d7c9bd7a490a34b334766df92964dec91fd5cc7e1768319c3b761a16ce91b980ec58ae990b2d2828a0e780a8e84967fa7c03cc052d842cdc645b47f3b000286cc8f28fee0542bd0a715c04816572412a78e07a595f646354de4e8959a80713401b9bae9624f6fd9828d1f36c440417ab6c6e4f7d3ce8f165529983ab31f1bf09eb67f7b19d6f192548cdb5f34accfdfd6a69f081cf9ffa9994cb5824ff1f89bd9915ceb08eb935c7d1181aba7da21f540b7351098fcfb708e91d65d7b091cb0b17dd60f241e87cd8e6635b0eba3b015185d25de049b63a7c32ce752989dac83f03a2af6664f6eb072202100a5a6d10a8ea84d2cc10872acafd6e2989c32baf41e3c4f3d5d55ec352f30517ebd74b96a10b80454e52009898eab5a6999d82a5829000182e20381e80220c128d6724fd7c641a959983b978d9fa693eb45ebfa44d0befd58193ac5f96c55d82a5828000181e2039220201026e6f2fadbf4f6d050d669a0d7171f9b602d906e9c51a6a311c86b5273dd2b000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x30520d4", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf14f04259ac556d1381965bd9d12c803e7e1ec83656ef48e3d57130963a8fd10", + "transactionPosition": 74 + }, + { + "type": "call", + "subtraces": 21, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cffc4", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x530c26f6", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000007128188828bd82a5828000181e203922020638379e47a3916bae849a7d2eb688c4c79f2a16dc32813f0fbc2799237183c251b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149677a69414f5733616d6d6f2f2f342b6f4e77425546714c787a57712b5365565143506f79524842494f4c46451a00381e4f1a004fa10f40480016de9bafa1a688405842018c87b3a0c61f501ebe01004d2c3989e74e8d3409e66ea7cf0485433f61ea84b92cab8863c916181ce0de0547b883855699afb28d70d8ed96c4fa49608949373100828bd82a5828000181e203922020a646954f2e700a51f149491da320e5065d7266a505b4013f3fe2b95a410487341b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d415843673541496777613542697561305a34474d65625947757336787a484e5a6e47726c744b456d7535744b3536676e366e551a00381e4f1a004fa10f40480016de9bafa1a6884058420144ad58edfe5d5d30d7b9789535d6c44c6f0014a05b3cb8e1141c1bf9a506d34c68d42e1f2b90ad6b13e231d84358535d36eb329f4e126dc5667eeebe8f87d40701828bd82a5828000181e20392202019553461c49b73c9ac6661d0bcbad1abd0b5747cb3b2c2cdbaaf2f0ee22e4f381b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149676d43454d37766c6163766b78657351395a6e785554564433696473695259644c613641636b4d6266514b381a00381e4f1a004fa10f40480016de9bafa1a6884058420111c340ca79b4ec152f975556bdca273716cd512588b630a12ac48109c4719e00406e504a97193fd228a1fa0492381acf41c3929fdc8f5a2d0cec1eef7216c79d01828bd82a5828000181e20392202078ef0796f6fb842665ab9c1896535d700bea5b1f70189272d8bea7a61b36f43b1b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d415843673541496749617553335659766c5578394e7575384c42776d507a55326f6d6e6a4c724e2b534474686439535656776b1a00381e4f1a004fa10f40480016de9bafa1a68840584201634845b6b07346e86e066a38f58506581f2c0edc279607b5d55f0245a2b0713046385efd92bd31fc2553d484d6067a699f1b9997dac16cd7f6467904f029f25501828bd82a5828000181e203922020997081e8865104540ec72c6f436a983e9633c5e5098d7ab01edf1a381c9495201b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d415843673541496742437463616277537077566247476239436d3068686a2b5075756e485471597a52694d41433676563879771a00381e4f1a004fa10f40480016de9bafa1a688405842013f72930bc2558ba9617bd708bd4abf74e197ba4483a97ced4bd8029bc0c5be26696583bf0c963021d7009827f800494e13e8cb5c0e17a44955cfe2739d8ed5c301828bd82a5828000181e20392202058a6d1ab9902fade4bf717e7331aee4b46fc210ba1b972dcf3ea2048a541463e1b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149674a4b56744f37483938726c477248634d5738466a5764644d6d532f54356a34334b4c4944563575713037491a00381e501a004fa11040480016de9ad5b2b21640584201df7baeaa70111f1bf9cabbebb63a42ed93369fdbc655810c47539e2109e457984c9dfb31ebbb414bf6bde203b8b16895df94722e92d76f49d075deb1528e4e1d01828bd82a5828000181e2039220204792c6cd5e8d978b7093e5d9a26a72c45fe7c815fad2bced0745522d5b2004261b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149677567622f52586644466330763238562b304e4d684c5263723376596f70682f455246325435742f354970731a00381e501a004fa11040480016de9ad5b2b2164058420119c8ffdccfa265a45e1f93b09d158cb6f6d9313a9acb81dbd40ac6006d25003e29d4bcc093e3cc3de10fb0a3f4221c8e98a8785e3c63fc8843786af1f975cd3001828bd82a5828000181e203922020ffcfabfe3a003640cba142966a19f85d1028b10edb5b0d647273c55857af431f1b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d4158436735414967764c754c47416773594e6a3972372f734b67656433356f6d6d31545141614830716d6b6d307a54796774411a00381e501a004fa11040480016de9ad5b2b21640584201b639096d1da0d389c6fd4d4104be639dbb22c591fe7bdebb2907aee734a9b73e3cb9f9262905b81f92997b92e23616cf3f635eef8405e4d794f80f4fa431c2e7010000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3374f7cd", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002d82881a045b57631a045b57641a045b57651a045b57661a045b57671a045b57681a045b57691a045b576a42140100000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000108a0a", + "gas": "0x52f45b15", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000014c1cb970000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000064500c4ffb3010000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x133019", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x52e08644", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x52cfba64", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 3 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x52bcb4e2", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e38258418c87b3a0c61f501ebe01004d2c3989e74e8d3409e66ea7cf0485433f61ea84b92cab8863c916181ce0de0547b883855699afb28d70d8ed96c4fa49608949373100589d8bd82a5828000181e203922020638379e47a3916bae849a7d2eb688c4c79f2a16dc32813f0fbc2799237183c251b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149677a69414f5733616d6d6f2f2f342b6f4e77425546714c787a57712b5365565143506f79524842494f4c46451a00381e4f1a004fa10f40480016de9bafa1a688400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2e9807", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 4 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x528ae02a", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e382584144ad58edfe5d5d30d7b9789535d6c44c6f0014a05b3cb8e1141c1bf9a506d34c68d42e1f2b90ad6b13e231d84358535d36eb329f4e126dc5667eeebe8f87d40701589d8bd82a5828000181e203922020a646954f2e700a51f149491da320e5065d7266a505b4013f3fe2b95a410487341b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d415843673541496777613542697561305a34474d65625947757336787a484e5a6e47726c744b456d7535744b3536676e366e551a00381e4f1a004fa10f40480016de9bafa1a688400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 5 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x526100b3", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e382584111c340ca79b4ec152f975556bdca273716cd512588b630a12ac48109c4719e00406e504a97193fd228a1fa0492381acf41c3929fdc8f5a2d0cec1eef7216c79d01589d8bd82a5828000181e20392202019553461c49b73c9ac6661d0bcbad1abd0b5747cb3b2c2cdbaaf2f0ee22e4f381b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149676d43454d37766c6163766b78657351395a6e785554564433696473695259644c613641636b4d6266514b381a00381e4f1a004fa10f40480016de9bafa1a688400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 6 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x5237213c", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e3825841634845b6b07346e86e066a38f58506581f2c0edc279607b5d55f0245a2b0713046385efd92bd31fc2553d484d6067a699f1b9997dac16cd7f6467904f029f25501589d8bd82a5828000181e20392202078ef0796f6fb842665ab9c1896535d700bea5b1f70189272d8bea7a61b36f43b1b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d415843673541496749617553335659766c5578394e7575384c42776d507a55326f6d6e6a4c724e2b534474686439535656776b1a00381e4f1a004fa10f40480016de9bafa1a688400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 7 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x520d41c5", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e38258413f72930bc2558ba9617bd708bd4abf74e197ba4483a97ced4bd8029bc0c5be26696583bf0c963021d7009827f800494e13e8cb5c0e17a44955cfe2739d8ed5c301589d8bd82a5828000181e203922020997081e8865104540ec72c6f436a983e9633c5e5098d7ab01edf1a381c9495201b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d415843673541496742437463616277537077566247476239436d3068686a2b5075756e485471597a52694d41433676563879771a00381e4f1a004fa10f40480016de9bafa1a688400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 8 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x51e3624e", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e3825841df7baeaa70111f1bf9cabbebb63a42ed93369fdbc655810c47539e2109e457984c9dfb31ebbb414bf6bde203b8b16895df94722e92d76f49d075deb1528e4e1d01589d8bd82a5828000181e20392202058a6d1ab9902fade4bf717e7331aee4b46fc210ba1b972dcf3ea2048a541463e1b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149674a4b56744f37483938726c477248634d5738466a5764644d6d532f54356a34334b4c4944563575713037491a00381e501a004fa11040480016de9ad5b2b216400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 9 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x51b982d6", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e382584119c8ffdccfa265a45e1f93b09d158cb6f6d9313a9acb81dbd40ac6006d25003e29d4bcc093e3cc3de10fb0a3f4221c8e98a8785e3c63fc8843786af1f975cd3001589d8bd82a5828000181e2039220204792c6cd5e8d978b7093e5d9a26a72c45fe7c815fad2bced0745522d5b2004261b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d41584367354149677567622f52586644466330763238562b304e4d684c5263723376596f70682f455246325435742f354970731a00381e501a004fa11040480016de9ad5b2b216400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 10 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x518fa35f", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e3825841b639096d1da0d389c6fd4d4104be639dbb22c591fe7bdebb2907aee734a9b73e3cb9f9262905b81f92997b92e23616cf3f635eef8405e4d794f80f4fa431c2e701589d8bd82a5828000181e203922020ffcfabfe3a003640cba142966a19f85d1028b10edb5b0d647273c55857af431f1b0000000800000000f55501d886fa8ef922a4be477ef30d932184be05323f2244008a944278346d4158436735414967764c754c47416773594e6a3972372f734b67656433356f6d6d31545141614830716d6b6d307a54796774411a00381e501a004fa11040480016de9ad5b2b216400000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x26f6e7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 11 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000007", + "gas": "0x508a6662", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000c26ddbd50000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000064500aa9b8b010000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2edc57", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000104f002b5ff708418d6c8000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [ + 12 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000007", + "gas": "0x4a86bae6", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000d7d4deed00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026f844500aa9b8b014200064e0003782dace9d9000000000000005902538288861a00108a0ad82a5828000181e203922020638379e47a3916bae849a7d2eb688c4c79f2a16dc32813f0fbc2799237183c251b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e203922020a646954f2e700a51f149491da320e5065d7266a505b4013f3fe2b95a410487341b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e20392202019553461c49b73c9ac6661d0bcbad1abd0b5747cb3b2c2cdbaaf2f0ee22e4f381b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e20392202078ef0796f6fb842665ab9c1896535d700bea5b1f70189272d8bea7a61b36f43b1b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e203922020997081e8865104540ec72c6f436a983e9633c5e5098d7ab01edf1a381c9495201b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e20392202058a6d1ab9902fade4bf717e7331aee4b46fc210ba1b972dcf3ea2048a541463e1b00000008000000001a001782c01a001b77401a00381e50861a00108a0ad82a5828000181e2039220204792c6cd5e8d978b7093e5d9a26a72c45fe7c815fad2bced0745522d5b2004261b00000008000000001a001782c01a001b77401a00381e50861a00108a0ad82a5828000181e203922020ffcfabfe3a003640cba142966a19f85d1028b10edb5b0d647273c55857af431f1b00000008000000001a001782c01a001b77401a00381e50800000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x397cbb2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000067844f002b5c7eda94a39380000000000000500006a8d4d4487a428de5110000000000520002f0503706f730bd424045568000000000583083820880820080881a034d18b51a034d18b61a034d18b71a034d18b81a034d18b91a034d18ba1a034d18bb1a034d18bc00000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 12, + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000007", + "to": "0xff00000000000000000000000000000000000006", + "gas": "0x475d9536", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000de180de3000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000277821a85223bdf59026e861a0022cdaa06054e0003782dace9d9000000000000005902538288861a00108a0ad82a5828000181e203922020638379e47a3916bae849a7d2eb688c4c79f2a16dc32813f0fbc2799237183c251b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e203922020a646954f2e700a51f149491da320e5065d7266a505b4013f3fe2b95a410487341b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e20392202019553461c49b73c9ac6661d0bcbad1abd0b5747cb3b2c2cdbaaf2f0ee22e4f381b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e20392202078ef0796f6fb842665ab9c1896535d700bea5b1f70189272d8bea7a61b36f43b1b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e203922020997081e8865104540ec72c6f436a983e9633c5e5098d7ab01edf1a381c9495201b00000008000000001a001782c01a001b77401a00381e4f861a00108a0ad82a5828000181e20392202058a6d1ab9902fade4bf717e7331aee4b46fc210ba1b972dcf3ea2048a541463e1b00000008000000001a001782c01a001b77401a00381e50861a00108a0ad82a5828000181e2039220204792c6cd5e8d978b7093e5d9a26a72c45fe7c815fad2bced0745522d5b2004261b00000008000000001a001782c01a001b77401a00381e50861a00108a0ad82a5828000181e203922020ffcfabfe3a003640cba142966a19f85d1028b10edb5b0d647273c55857af431f1b00000008000000001a001782c01a001b77401a00381e508040000000000000000000" + }, + "result": { + "gasUsed": "0xa1c4027", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000003083820880820080881a034d18b51a034d18b61a034d18b71a034d18b81a034d18b91a034d18ba1a034d18bb1a034d18bc00000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 13 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x10787f37", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e203922020638379e47a3916bae849a7d2eb688c4c79f2a16dc32813f0fbc2799237183c251b0000000800000000f54500aa9b8b0144008a944278346d41584367354149677a69414f5733616d6d6f2f2f342b6f4e77425546714c787a57712b5365565143506f79524842494f4c46451a00381e4f1a004fa10f40480016de9bafa1a688401a045b57630000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 14 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x106e1d88", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e203922020a646954f2e700a51f149491da320e5065d7266a505b4013f3fe2b95a410487341b0000000800000000f54500aa9b8b0144008a944278346d415843673541496777613542697561305a34474d65625947757336787a484e5a6e47726c744b456d7535744b3536676e366e551a00381e4f1a004fa10f40480016de9bafa1a688401a045b57640000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 15 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x1063bbd9", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e20392202019553461c49b73c9ac6661d0bcbad1abd0b5747cb3b2c2cdbaaf2f0ee22e4f381b0000000800000000f54500aa9b8b0144008a944278346d41584367354149676d43454d37766c6163766b78657351395a6e785554564433696473695259644c613641636b4d6266514b381a00381e4f1a004fa10f40480016de9bafa1a688401a045b57650000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 16 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x10595a2a", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e20392202078ef0796f6fb842665ab9c1896535d700bea5b1f70189272d8bea7a61b36f43b1b0000000800000000f54500aa9b8b0144008a944278346d415843673541496749617553335659766c5578394e7575384c42776d507a55326f6d6e6a4c724e2b534474686439535656776b1a00381e4f1a004fa10f40480016de9bafa1a688401a045b57660000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 17 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x104ef87c", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e203922020997081e8865104540ec72c6f436a983e9633c5e5098d7ab01edf1a381c9495201b0000000800000000f54500aa9b8b0144008a944278346d415843673541496742437463616277537077566247476239436d3068686a2b5075756e485471597a52694d41433676563879771a00381e4f1a004fa10f40480016de9bafa1a688401a045b57670000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 18 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x104496cd", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e20392202058a6d1ab9902fade4bf717e7331aee4b46fc210ba1b972dcf3ea2048a541463e1b0000000800000000f54500aa9b8b0144008a944278346d41584367354149674a4b56744f37483938726c477248634d5738466a5764644d6d532f54356a34334b4c4944563575713037491a00381e501a004fa11040480016de9ad5b2b216401a045b57680000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 19 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x103a351e", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e2039220204792c6cd5e8d978b7093e5d9a26a72c45fe7c815fad2bced0745522d5b2004261b0000000800000000f54500aa9b8b0144008a944278346d41584367354149677567622f52586644466330763238562b304e4d684c5263723376596f70682f455246325435742f354970731a00381e501a004fa11040480016de9ad5b2b216401a045b57690000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 20 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000022cdaa", + "gas": "0x102fd36f", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009582588d8bd82a5828000181e203922020ffcfabfe3a003640cba142966a19f85d1028b10edb5b0d647273c55857af431f1b0000000800000000f54500aa9b8b0144008a944278346d4158436735414967764c754c47416773594e6a3972372f734b67656433356f6d6d31545141614830716d6b6d307a54796774411a00381e501a004fa11040480016de9ad5b2b216401a045b576a0000000000000000000000" + }, + "result": { + "gasUsed": "0x98587", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc72dce371722de2e44a8b2f9b33551bd2dd5ae8595caafe3d34b9f15b01734ad", + "transactionPosition": 75 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c17d8", + "to": "0xff000000000000000000000000000000001c17eb", + "gas": "0x4af5d01", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000072818187081a000122f0d82a5829000182e20381e802203050bd59cec207bc1408562d631cd13b7a00056b34e3dc612288cec5950573111a0037e0c0811a045b35581a00412a9ad82a5828000181e203922020e296585450fb35885860e0c4ac16cc5a7b1013b0f76d736b884763912a3e7e080000000000000000000000000000" + }, + "result": { + "gasUsed": "0x362e335", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x78132bb8f711bfc5da6a26980693e7256b6ba22e5d829b3d2e8614ba26b0a953", + "transactionPosition": 76 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c17eb", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x4a3f6b1", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x78132bb8f711bfc5da6a26980693e7256b6ba22e5d829b3d2e8614ba26b0a953", + "transactionPosition": 76 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c17eb", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4933169", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x78132bb8f711bfc5da6a26980693e7256b6ba22e5d829b3d2e8614ba26b0a953", + "transactionPosition": 76 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001c17eb", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x4811bf8", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a00412a9a811a045b35580000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x478528", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020e296585450fb35885860e0c4ac16cc5a7b1013b0f76d736b884763912a3e7e08000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x78132bb8f711bfc5da6a26980693e7256b6ba22e5d829b3d2e8614ba26b0a953", + "transactionPosition": 76 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0815", + "to": "0xff000000000000000000000000000000002d082d", + "gas": "0x512a1bc", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782194e995907808c80a1a7f1aef23e9c5a72f013f261279d9c83df068122083bfc73c928562b99d7e142e8323c3b1ba398efc3fddd17d686367ba915da2e8f05f5605d58f2957927a51de60a07ab0dbd24ac1e8a52b6af11b95df831d14dbac25d750fd4a45dca0837df29befcba63b8178ea8f02340b6d7d6bdb058bfc565ded5260ada6394f7c68c9a4d8494e84eeac2866efd78a5ab8995d7fa578bdc9df4b54ede4212535f7f890a681a729126bc2012fbb57b1cb3b59a93bec4617ee15ad138e9985b14b4955cd39414d99e873f5f4647c36d32362ee8d01c892a0117abed4d76e5f4f8fffbef81f01b535662b42ce3ccf5b415a2b8a671847288a295efc0f210cd27009e8084a633405e7f458cf404909035e07384d4f599bc4d7a2de45c9630b2b12c33072bff14f9e8dc7cbefc0f2573c2227e89711e63ae803944745a28949dc0ac962d4b8faccb5730b1a5e95e59c269dc1fb1a45fe8186d56382cfcc8c2cb70e93f01a16f059d08f5cde17a7fc62454ec77182142a08e69d2b2b4ff48333542077db708bd647e0ac8f4d1db47b183c91f6084957b28fb75778288f8b83975bc630c1ee44dcd9a9b95c3e440aac0bfe88683986dcca45b0e4c16286c3a2aeb9f7e1d43b914cd0b3f629ee426feff417c321ddb9b798070e040a269c9e22b1e1a81e20931d105241068cf3ae4792bda0f15593e4482dfeeae4e2f0386f08eb5cf7f1bbec416fa4378dfd75924a56d04610790aeb39aaaa8c2840f7c52fcb8f89a8b2d7cc4336ecac6d9e40223d45b31f2926dd0e45a1bfb6f0b6cfb5f4d0f0d8f9255aa0021ea31b75191ea0159be650d495e4dbe608db1ecfba2da7de60a321e5f7da691ad4db7db3b5c7885ad51cea72d38ad5c0eb37188c86cbe01c938896c0b6893c8698a5f7f1eb5cea5cdbcd39f718098086ea5be03d25c5d9ab5a30d96b78308b14a7ca1b098b8045c4d90a78206fc18fe6e1cf8c4cca2cda8dcd6b345211e4d8841a90978a7a21c58474c06aa5e568adaaa9fa880ac84956894d2e976d459bed01056a00e69bfccfe9d47f65fdef85720468801603ba01a63865c99acba06990df8d8ada941b6ebedc7484a9a574a18b16cea4683a8bfd6a90e1d845fa4ca568f3e4ea10818a8b551ee7defa34f7d95a71ed3bd40ceb688ec6c00cc207ce8305060b96a5acfb86c38a514bc3223a21e36a2212bf47bef2c2e6b3a93ad463501f46a336efed31b3d9e9998dbb10362e86135838c62a8fbb0489c18f72110dd2fc63428d2dd7e4351b2e075ffc89d9f99a1c2e049c1b5c7ddcebf1972cc4208122a1c981f44bd5c4810a2233922ceadebff26e745d4dba5cd698659399f6eb4b80e14b0f5b2d3604c971c4d6d5a2a3aad696a5660ceb110379fad11e21bcbcf82cc7c9de0ea2afbc0a90c4e22b1562cb1f436a758f2550d8ec0f06ea21e2a230e94e2b1eb5f3dba6341a01dcb08c9c9e19a68e1302b6ec8d9453214ec9340261329fc6389bbe2ee256e72c6ee1440ec2cd7d6e9a0ccaa1676826978e9763fdb9883efd5856573bffda76b38e20a84a48ef7aa8f17163962498dad8493ee48f8f0aca45098fc026df4b3dc20231bb647d894380c97bdedcd652bce638f65cb1b82c672e1aba05a9220b3540cbfed4bc74626309f44fd91d4d666349fd06ac0af5eaa45e37913a9ac44edd81d852c06f2a4fc567dc4c88164be2e004c7c4adc45223a4f6c0e34056c15393ef77c4a6f7f8ebd40774402335828324485c93f37cc01627a3234c1544fbda48a4d48be691d60055a3dbc38890cc0a5c9c89b4487f9865c0279f8898b77f66ee7c0b5f60b7f93cff14cffbd5465f61130bcee8107b17b6f761697f9008048dee7d0d3ae8519d43e3769b6c954fbbde729cd7c2fb86f97a78a947df5709f793c3d532e1dc4f81943b2f4e3502ffa30bd2d0da55c7661d6e779218c9b5708687671a0d2c7aa11962792e3ceb0cd8fd43824d6076c9566cc9fcf53af9c5ca560d6c2f73032ad1bc2a6b7667a4d925604e8b0ae8b55152a14ca4945e04f764f32c499d127884d6aa62fcea45b9c8d4a4f92cef803c55f66794f15cd5408dd57b802f4b95076a42eaf8bc88cae56becbdbb80130d2a52271d56c12876c2468726b439ea6e3558513790133e1dabb8574bb9cb99e70062f8cb206a536d8531deef4622096152e2e11d781b076e340edead07f4d34716bb6a5e6f0f26d3db1ba392c423857d9a2415eb660aae7b35aea3690a916e06ca2f927c5746b850f48239eec274963a123579ff3c33cdd40c83f04c6bfb307827953be12769e909af08c922f5872b8091629d637234884ed042edcca7c763e92163bc5e1ef1dfa28bd61ff429eb805a50e5cd798e723a051b57f08f9d4cebb4c5b5d78c8941e7a506d48fbf39b62c0a9255ed8a21170fecc1749cbb3b3ce64becf159388dc98ef1ce0163e12083ebafe678083245e05fb0fe66eefde1a1af1cb5529efe6897d7982a0a52fe1a6eb7c2d0df9568427e81a5c2dc38229752c83511345b03aaf28e40f40d6437410404158149cc36906067c6228cf679e1edc64b37abab0026c2fbf628c75c140466df6dd059ed0f4ab80b87bd7b107d8d5a1d40238c594468b699a34606c6a45aab2b7e834df48b7b2956fd06ccbffaded621041a9d8f78061daba66f2f943910c69ff71169a5a2464e05895a10ed0c38973d188afbd1500000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x88fc3f", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf9b0bea08c4a70da033330b656816716dd0bb2907a20fbb247e02bd6f5a7506c", + "transactionPosition": 77 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d082d", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4ba7b2f", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002d082d194e99811a045b25435820e1d4ff2fffdc2d955643405d87b009d9b4e4416564a87718a503d569c42a9c13582079dec049eddef67a2d8acaafb048cce7ccea8cd69df4b68830bb33041f5914c25907808c80a1a7f1aef23e9c5a72f013f261279d9c83df068122083bfc73c928562b99d7e142e8323c3b1ba398efc3fddd17d686367ba915da2e8f05f5605d58f2957927a51de60a07ab0dbd24ac1e8a52b6af11b95df831d14dbac25d750fd4a45dca0837df29befcba63b8178ea8f02340b6d7d6bdb058bfc565ded5260ada6394f7c68c9a4d8494e84eeac2866efd78a5ab8995d7fa578bdc9df4b54ede4212535f7f890a681a729126bc2012fbb57b1cb3b59a93bec4617ee15ad138e9985b14b4955cd39414d99e873f5f4647c36d32362ee8d01c892a0117abed4d76e5f4f8fffbef81f01b535662b42ce3ccf5b415a2b8a671847288a295efc0f210cd27009e8084a633405e7f458cf404909035e07384d4f599bc4d7a2de45c9630b2b12c33072bff14f9e8dc7cbefc0f2573c2227e89711e63ae803944745a28949dc0ac962d4b8faccb5730b1a5e95e59c269dc1fb1a45fe8186d56382cfcc8c2cb70e93f01a16f059d08f5cde17a7fc62454ec77182142a08e69d2b2b4ff48333542077db708bd647e0ac8f4d1db47b183c91f6084957b28fb75778288f8b83975bc630c1ee44dcd9a9b95c3e440aac0bfe88683986dcca45b0e4c16286c3a2aeb9f7e1d43b914cd0b3f629ee426feff417c321ddb9b798070e040a269c9e22b1e1a81e20931d105241068cf3ae4792bda0f15593e4482dfeeae4e2f0386f08eb5cf7f1bbec416fa4378dfd75924a56d04610790aeb39aaaa8c2840f7c52fcb8f89a8b2d7cc4336ecac6d9e40223d45b31f2926dd0e45a1bfb6f0b6cfb5f4d0f0d8f9255aa0021ea31b75191ea0159be650d495e4dbe608db1ecfba2da7de60a321e5f7da691ad4db7db3b5c7885ad51cea72d38ad5c0eb37188c86cbe01c938896c0b6893c8698a5f7f1eb5cea5cdbcd39f718098086ea5be03d25c5d9ab5a30d96b78308b14a7ca1b098b8045c4d90a78206fc18fe6e1cf8c4cca2cda8dcd6b345211e4d8841a90978a7a21c58474c06aa5e568adaaa9fa880ac84956894d2e976d459bed01056a00e69bfccfe9d47f65fdef85720468801603ba01a63865c99acba06990df8d8ada941b6ebedc7484a9a574a18b16cea4683a8bfd6a90e1d845fa4ca568f3e4ea10818a8b551ee7defa34f7d95a71ed3bd40ceb688ec6c00cc207ce8305060b96a5acfb86c38a514bc3223a21e36a2212bf47bef2c2e6b3a93ad463501f46a336efed31b3d9e9998dbb10362e86135838c62a8fbb0489c18f72110dd2fc63428d2dd7e4351b2e075ffc89d9f99a1c2e049c1b5c7ddcebf1972cc4208122a1c981f44bd5c4810a2233922ceadebff26e745d4dba5cd698659399f6eb4b80e14b0f5b2d3604c971c4d6d5a2a3aad696a5660ceb110379fad11e21bcbcf82cc7c9de0ea2afbc0a90c4e22b1562cb1f436a758f2550d8ec0f06ea21e2a230e94e2b1eb5f3dba6341a01dcb08c9c9e19a68e1302b6ec8d9453214ec9340261329fc6389bbe2ee256e72c6ee1440ec2cd7d6e9a0ccaa1676826978e9763fdb9883efd5856573bffda76b38e20a84a48ef7aa8f17163962498dad8493ee48f8f0aca45098fc026df4b3dc20231bb647d894380c97bdedcd652bce638f65cb1b82c672e1aba05a9220b3540cbfed4bc74626309f44fd91d4d666349fd06ac0af5eaa45e37913a9ac44edd81d852c06f2a4fc567dc4c88164be2e004c7c4adc45223a4f6c0e34056c15393ef77c4a6f7f8ebd40774402335828324485c93f37cc01627a3234c1544fbda48a4d48be691d60055a3dbc38890cc0a5c9c89b4487f9865c0279f8898b77f66ee7c0b5f60b7f93cff14cffbd5465f61130bcee8107b17b6f761697f9008048dee7d0d3ae8519d43e3769b6c954fbbde729cd7c2fb86f97a78a947df5709f793c3d532e1dc4f81943b2f4e3502ffa30bd2d0da55c7661d6e779218c9b5708687671a0d2c7aa11962792e3ceb0cd8fd43824d6076c9566cc9fcf53af9c5ca560d6c2f73032ad1bc2a6b7667a4d925604e8b0ae8b55152a14ca4945e04f764f32c499d127884d6aa62fcea45b9c8d4a4f92cef803c55f66794f15cd5408dd57b802f4b95076a42eaf8bc88cae56becbdbb80130d2a52271d56c12876c2468726b439ea6e3558513790133e1dabb8574bb9cb99e70062f8cb206a536d8531deef4622096152e2e11d781b076e340edead07f4d34716bb6a5e6f0f26d3db1ba392c423857d9a2415eb660aae7b35aea3690a916e06ca2f927c5746b850f48239eec274963a123579ff3c33cdd40c83f04c6bfb307827953be12769e909af08c922f5872b8091629d637234884ed042edcca7c763e92163bc5e1ef1dfa28bd61ff429eb805a50e5cd798e723a051b57f08f9d4cebb4c5b5d78c8941e7a506d48fbf39b62c0a9255ed8a21170fecc1749cbb3b3ce64becf159388dc98ef1ce0163e12083ebafe678083245e05fb0fe66eefde1a1af1cb5529efe6897d7982a0a52fe1a6eb7c2d0df9568427e81a5c2dc38229752c83511345b03aaf28e40f40d6437410404158149cc36906067c6228cf679e1edc64b37abab0026c2fbf628c75c140466df6dd059ed0f4ab80b87bd7b107d8d5a1d40238c594468b699a34606c6a45aab2b7e834df48b7b2956fd06ccbffaded621041a9d8f78061daba66f2f943910c69ff71169a5a2464e05895a10ed0c38973d188afbd15d82a5829000182e20381e8022046a040e4fb486d77bd3aefb25a4eb3670e46e3f935fade483164764113c9092ad82a5828000181e2039220208e04de2905b7795a206f4ca5423c6a65dabfd8304b05b92aeb298af986ff1c01000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x309b9c7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf9b0bea08c4a70da033330b656816716dd0bb2907a20fbb247e02bd6f5a7506c", + "transactionPosition": 77 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d0815", + "to": "0xff000000000000000000000000000000002d082d", + "gas": "0x4d995a0", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782194e7659078099831f9576b59ca994613a166b25313bd47dfea452d5fca7152208025b41569c4b746255e1f9b84c29ae1a2fd7b51c41b8ef90add5e5507af6fc9477acf8a4e467d92a6f8b9192187d58a63cfebbbb58db31e89a5cb0cec05afa21126a9f1f4d12a51c2bf2630a8f35de9f5b5917dbbd53fb2a80b940dba66b544572dcc9442f0b504c48d4af9930d653ad9e5de6d407b58f2ea14e8c4d0daa6b1ba3a9ee6aa6336bfe6a6d62b9b76bd8ce9a3cb83b67c98695a0d83762582a3c0a0f9ee2e4f081b5f7573a0879bef50e2833c42862f1c18a880b9caa2a55f0eb415e8394fef0c5e681cb33cb06532f49658ffe675138b16d27817e745e0b0cc3928eaf99644f8eaebb6f1679a0a9fb3814a3342004b5885dc1a14a27b4fa64286dd0a076e3b60276a16e86e2736bd0a808734caede2fb2f09db852720f3e531d3226ef7c42dab93285600995614ce01dbdf40c0a2450ae04149d63878dee9f00c37980523cadd7570c2e9fe5ed4b2b33a16f510e5cdfe2add1e44df129097b71a25f918bc5ed80278fa2c13977ce824c8772f285c8ff2cae6502d59f46965a18169fae74f22801c0360ee99355f2d05e8cae2635e9148d397467af6b2288b27f2104fce3a1de18900fe71808c5d30b050f2a6842794dc547b49ca9c8d9eafd6dc5e40f9999ad0d06dfdf3a3148d6504816215035bf4ef67f767196837fd6332179e426838a1a6015d1ed9eecce775bb1cf805beeb22f96ccbed2588efcee9e0900ca3d1dcda9cd9fb1b74d1421f69e9f64116ca7f847824983039a2cf530ab96016bb625df50b880b01ea628f7bac496a4884a2be94a8bae8d344f0e4ebcb1910cf9d4547df86ba460645e4803f4a9f94b1ab3385552872be6f3a8dfb1993843ff36dd9f84f8bea6dafdf293a110d6617c14d2f9baed553c8a13fc1b711f1eeb45eb24a4ec0407f15431b667d49129837af9ed1dd815feb000650de073f80802572806947ae12feb9068dab35f2b3ba34a2fa9a4086187a81a66b249c8bee5b6ceb4bd8e453db444ef365eeea12e1e2ad3b656a2071a21512e972a99b7578898f82e458a6dcb91f2fe327f52b66d72167780f91bf3087924285f659f13900e441d56720ad2706460b970a36a67ec7d4ae41dbb0c63e490fc93ce81e6462b22842cc6fcdcda160f534531f6142ec8b6c4a46b506e1098e19994af6317be2aaa17f028adc4c43a18a2b77b16b1504c73782e5a9d0cdae8ad687921e1e5a1994c1cf0a46cd6cb78b318e72fdcd162adf463642e7bc48a6c92028af24770f3456e2dec29a3f3dc5b310183734a2c2f736a28807168b68e1985d39c39586eab9ded0df6d5643cd41eaee50789eda6cee7540552c9ec67094db48f7d7053605b266747545e72283d5640548aac9fce739dd4004d4ef869a72480a04609bd8f87054815b3cdfb18929784b1c12d8d6aa81bb417625924df826dcc025a1cce3c7203d906c6f25d7a863b1831bfb61737060a63707c5308f125fde71c50686e8813373aea7451e15e1579508a43b7bf9ac23546bfe1e5d80de563979d455615f690a07a44b2e7f4fa8460f1a1ce0a428495e3746cca143099ea00351ab4e001eeb3a8f2f8dc7ceaa06e9680424438aa2401867f354e33808fee11abc04240d19eeb91fd9551fb828a32cbd7e3e1b924247bd77f37335c957be1af84013c9ed4ad5beaee30bf3838c9bdaa4b4c9c6ef4b838360983426cde4454d1220f8677da23a117844bb82e168c51781222bc620ad9e8341141391b7f1e74ba73c751b47a8c2a44bbf90f2926a19fef298afdf0ed34c7f34e8c3f3339c653a7adb80caf0d61be88fb521e254a313d3118f0d59d9a77b5b29d321c08c739fd7256bc4cf42fa1b3c200bddfeea074843a99e8b14bc42ec667fc1a46356e3e18aea5bb5cbb981e867e2a9f26e4c00e4f922741e132900115257ece0265cdc485a9a6b1e2141fb5048e05e911a34b14eccb6c9cc5ad8af7be6cb6c5d9159832640d0773f5b3cb57cb4f85d9673db29632bc0c5bc7d26db2d396c8d74932fc3a7670440b76ea62b96e69dca52efccfebc9ae594b3a0e26143124d8a84ac427cd974889dd6b74405dd8d2367a7ab0e4aeec9b51b1e96f4819f48deb0afb32d9b0b9bbc4701c9d60da394dcea1d86c98edb8b6a065bc0c19840d2e6f454459f9aeedb422addd265807259f0dfceb1321cb3acd3b40584bf8b3340066580a27ce0c9b289959fce200924097e34d457096a4c5e3c3da0e644d665c071d75b3f7828361f7d801e48527af973586fbd0c07de1925a07388f606844c7c379a347b3439648a6752e95a4e89fd64823c8ac619459b93cf7716c1dcea6ddd460f4c42eb9994ff98a8b00cf9a74b13152e9fb1bfcf0b00d45869bdfd88355ec1d473a8752bf1a2a04640ab3918bc4f0bef6b9187b74c414b0d1ee19ff3a1aeee50b73300d3010bd2698ca444c629509acce503d59248b475602fe48abea5a4b8da9886e71d00f4ba62142c8790b630b357014be80b83aadd3784423b90d54cce1ac94297a694e0da2f284a0af093c1a73557ff126cfd29e035d59c283c826d17da2af6d967936735edd32c4e3dbaddc3b113a2a03c2b7ee953b194f05f2b17e54fa36fb3b30396fb858ef99398e82a3ea62113cac8f1a216017083de9d1c5524b777dfba1051c3bc2d510cfde0fb762cea5f226d8b6e64b00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x89592a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x11af98bb4b63e184700045f41d8370daa555dc189c7670a92b59c4fc7c47ff06", + "transactionPosition": 78 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d082d", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x48112a4", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002d082d194e76811a045b225a58208e503e3d366299b9095156df2ccafbc49fbe238c3592a879c7cbc3fa095d26545820f7f9630e9e1ff4c01611a3dd41958c27ef95804e97da3fafe1e0c27da131a91459078099831f9576b59ca994613a166b25313bd47dfea452d5fca7152208025b41569c4b746255e1f9b84c29ae1a2fd7b51c41b8ef90add5e5507af6fc9477acf8a4e467d92a6f8b9192187d58a63cfebbbb58db31e89a5cb0cec05afa21126a9f1f4d12a51c2bf2630a8f35de9f5b5917dbbd53fb2a80b940dba66b544572dcc9442f0b504c48d4af9930d653ad9e5de6d407b58f2ea14e8c4d0daa6b1ba3a9ee6aa6336bfe6a6d62b9b76bd8ce9a3cb83b67c98695a0d83762582a3c0a0f9ee2e4f081b5f7573a0879bef50e2833c42862f1c18a880b9caa2a55f0eb415e8394fef0c5e681cb33cb06532f49658ffe675138b16d27817e745e0b0cc3928eaf99644f8eaebb6f1679a0a9fb3814a3342004b5885dc1a14a27b4fa64286dd0a076e3b60276a16e86e2736bd0a808734caede2fb2f09db852720f3e531d3226ef7c42dab93285600995614ce01dbdf40c0a2450ae04149d63878dee9f00c37980523cadd7570c2e9fe5ed4b2b33a16f510e5cdfe2add1e44df129097b71a25f918bc5ed80278fa2c13977ce824c8772f285c8ff2cae6502d59f46965a18169fae74f22801c0360ee99355f2d05e8cae2635e9148d397467af6b2288b27f2104fce3a1de18900fe71808c5d30b050f2a6842794dc547b49ca9c8d9eafd6dc5e40f9999ad0d06dfdf3a3148d6504816215035bf4ef67f767196837fd6332179e426838a1a6015d1ed9eecce775bb1cf805beeb22f96ccbed2588efcee9e0900ca3d1dcda9cd9fb1b74d1421f69e9f64116ca7f847824983039a2cf530ab96016bb625df50b880b01ea628f7bac496a4884a2be94a8bae8d344f0e4ebcb1910cf9d4547df86ba460645e4803f4a9f94b1ab3385552872be6f3a8dfb1993843ff36dd9f84f8bea6dafdf293a110d6617c14d2f9baed553c8a13fc1b711f1eeb45eb24a4ec0407f15431b667d49129837af9ed1dd815feb000650de073f80802572806947ae12feb9068dab35f2b3ba34a2fa9a4086187a81a66b249c8bee5b6ceb4bd8e453db444ef365eeea12e1e2ad3b656a2071a21512e972a99b7578898f82e458a6dcb91f2fe327f52b66d72167780f91bf3087924285f659f13900e441d56720ad2706460b970a36a67ec7d4ae41dbb0c63e490fc93ce81e6462b22842cc6fcdcda160f534531f6142ec8b6c4a46b506e1098e19994af6317be2aaa17f028adc4c43a18a2b77b16b1504c73782e5a9d0cdae8ad687921e1e5a1994c1cf0a46cd6cb78b318e72fdcd162adf463642e7bc48a6c92028af24770f3456e2dec29a3f3dc5b310183734a2c2f736a28807168b68e1985d39c39586eab9ded0df6d5643cd41eaee50789eda6cee7540552c9ec67094db48f7d7053605b266747545e72283d5640548aac9fce739dd4004d4ef869a72480a04609bd8f87054815b3cdfb18929784b1c12d8d6aa81bb417625924df826dcc025a1cce3c7203d906c6f25d7a863b1831bfb61737060a63707c5308f125fde71c50686e8813373aea7451e15e1579508a43b7bf9ac23546bfe1e5d80de563979d455615f690a07a44b2e7f4fa8460f1a1ce0a428495e3746cca143099ea00351ab4e001eeb3a8f2f8dc7ceaa06e9680424438aa2401867f354e33808fee11abc04240d19eeb91fd9551fb828a32cbd7e3e1b924247bd77f37335c957be1af84013c9ed4ad5beaee30bf3838c9bdaa4b4c9c6ef4b838360983426cde4454d1220f8677da23a117844bb82e168c51781222bc620ad9e8341141391b7f1e74ba73c751b47a8c2a44bbf90f2926a19fef298afdf0ed34c7f34e8c3f3339c653a7adb80caf0d61be88fb521e254a313d3118f0d59d9a77b5b29d321c08c739fd7256bc4cf42fa1b3c200bddfeea074843a99e8b14bc42ec667fc1a46356e3e18aea5bb5cbb981e867e2a9f26e4c00e4f922741e132900115257ece0265cdc485a9a6b1e2141fb5048e05e911a34b14eccb6c9cc5ad8af7be6cb6c5d9159832640d0773f5b3cb57cb4f85d9673db29632bc0c5bc7d26db2d396c8d74932fc3a7670440b76ea62b96e69dca52efccfebc9ae594b3a0e26143124d8a84ac427cd974889dd6b74405dd8d2367a7ab0e4aeec9b51b1e96f4819f48deb0afb32d9b0b9bbc4701c9d60da394dcea1d86c98edb8b6a065bc0c19840d2e6f454459f9aeedb422addd265807259f0dfceb1321cb3acd3b40584bf8b3340066580a27ce0c9b289959fce200924097e34d457096a4c5e3c3da0e644d665c071d75b3f7828361f7d801e48527af973586fbd0c07de1925a07388f606844c7c379a347b3439648a6752e95a4e89fd64823c8ac619459b93cf7716c1dcea6ddd460f4c42eb9994ff98a8b00cf9a74b13152e9fb1bfcf0b00d45869bdfd88355ec1d473a8752bf1a2a04640ab3918bc4f0bef6b9187b74c414b0d1ee19ff3a1aeee50b73300d3010bd2698ca444c629509acce503d59248b475602fe48abea5a4b8da9886e71d00f4ba62142c8790b630b357014be80b83aadd3784423b90d54cce1ac94297a694e0da2f284a0af093c1a73557ff126cfd29e035d59c283c826d17da2af6d967936735edd32c4e3dbaddc3b113a2a03c2b7ee953b194f05f2b17e54fa36fb3b30396fb858ef99398e82a3ea62113cac8f1a216017083de9d1c5524b777dfba1051c3bc2d510cfde0fb762cea5f226d8b6e64bd82a5829000182e20381e80220f35099649c579a0471c666d7319476b5bdc31aa0d651e214daae463001788769d82a5828000181e2039220200eaa7afc2292c2eb6a2db7208cd738b6b5b7fb69577b134cf1817c6f4d39dc0d000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x37f99a8", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x11af98bb4b63e184700045f41d8370daa555dc189c7670a92b59c4fc7c47ff06", + "transactionPosition": 78 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001ca66f", + "to": "0xff000000000000000000000000000000001ca698", + "gas": "0x336d63a", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000027a8518228382004082014082024081820d590240a08ff3b7b6fac42168d1bb9fe5ec46d2a1f0f7393abe61635e7cd7abb92358fc5b06b05b5598363c685a19a2eb0a789a974237d01d4e9abab7fd26c5f5b961c3c2e7e7a02f811aef4e9017183f01fbf2fcec48c11c66b04ec88c6205326d942c0481a6cd1f2a9ce933999b9f1ca17e7e250dace30151e38d334fd375c80942e881b4b75905866110ff9a8767765908bea19acda6d804c59114151663ef6aed4a26048afbc1b3541673ee9b223dec1adbc7e0317d90c570b2aa0ef83ca19fdc649143af73cce6bc1b5a2710b6da1abc090b08f26d9f14d4db79460f04803bfd718988bc44a81936da65d2631296f56489b8cce5f35c108aaf789d8f51cbccb80974d86b100876b421af58cc973a25363ad0e465c91a650677fd76e3d82d028dd910925f07bb1fe8e81bb8bbea9661f6bab48cd6a72c0fd8a70edb644cb4f1cb5c51be9168b28a2eba6e429f0fd69657fd88e05ef08bb018310481be280d48d435cf22bc452d61a846bae765092fd51625d5cea1a4d2b1fc2064b7b682393d35348c988668c6d985754deaa21b4ec3e7411268453a8f118295888def479a01e0c3a8fa274ce6c75cfe7ddd1f32d033a43080a25e8579a26b7dc65c1de7e88f22234842ce224f4f946717a7ada60282758ee63a6a08fb59f73ee5419aaeceeaac6511e0b58e0e14906e2e9343e52d9e6d05d4149ee4777bbfadb41f1d822dc68c5d5db3e1cc7d1cf8af15a7f2f874bbd867b13c74e229c51a1fe2e34d3e141592e5d16406695de5c9d4044770ccc776e49db6fdd3ac048ecf32d813e0e3fb883dc61a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d69000000000000" + }, + "result": { + "gasUsed": "0x2a5696d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xab7a2853fd0c3c51361087f07a575acf5ac0930968bef1ac73bf360275a52281", + "transactionPosition": 79 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000014b4b4", + "to": "0xff0000000000000000000000000000000014b4ca", + "gas": "0x27c657d", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001b785182f8282004082014081820d590180a6333476a9dc6aaee94d32d85f613057168b6aca7e43fcd54ee38b03aae16a0917c867e0339d5f492cd987024f4141a0966b843654b2e9e7af330fe6d83fa05630c3aff5417eeb00e5e3aafce6d12ffdcd18b68cea29003838faee385a3626330a8e3c20ec69cdb8e3b5ce8594c9c9eaf68996771fc6b8399edd629b97856d7611475591ba61b86aafba95ebb482ec5498d88485f2cb9d6920fd45dd8a234091a31014aa836496dced595b0f5f89aa7abfa5c5eae63a11e93c0a3b241cf42901b931570b6fe9f665a4e62be9d3b1cfc5c3d915edc6b899b28f9e49ed67157375d74210aa54d116b9af6d1da31095d2eaa8a5d81d48da35aa43436e12c6d1a539c691aaf6a3575dcf80219a0214b5babfb9b8811e55ec3afd1b9fd73f1b82dff707ea2962acc711288fa110af7e7671ec90b1a7f0f40534c7fc9b538a09fd3e89a5e38de44b12b279f952775aa0075d8c9840f0bbb2c40c0a5c4b999329da775933d297d4c97101cb13ed3192edac7596df7ac4533cf1a38b07fa1e18b58b11eb1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d69000000000000000000" + }, + "result": { + "gasUsed": "0x20d2a3e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x395ddc88f44eadde7eab48be036779baba3f53083167c7517a713f15c96bc804", + "transactionPosition": 80 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000112c55", + "to": "0xff00000000000000000000000000000000112c87", + "gas": "0x184c153", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f385181e8182004081820d58c0b6ea07c16c33360054815486dabadebe23a05ea409bd99d4fbf7ee738771daa5b76f68d46c3a59f42766ef147e7ed9a299dfe9d4990acbcd4aa5c21ba31e98b599d8f904c11c3a312877e1ed3e9d76581bf5b1b6c37872136a084bf5e7be564a150ed4a52079e13f4fa2a95946893e961ed7ff6753f8412494ba67faca84a2d912a51d9388bbdee6338a6b58bf7283ae85f28878222de7ceea33fa13d229b5cb1ba5a74186feabf2752d0329b868f9e92f5cc92bc1f120bd2d1f8eccabb8ef0d1a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d6900000000000000000000000000" + }, + "result": { + "gasUsed": "0x143ecfa", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x865c98e58b41449ae2484a8b15d7a5fa601779e0a55194cc8ccf5276556c532c", + "transactionPosition": 81 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ae495", + "to": "0xff000000000000000000000000000000002b1e7f", + "gas": "0x526070a", + "value": "0xf2ac3304b3a9ee7", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821958b3590780b104649bcc2962b63068e99c9027750b8c0624579c9782df07b725bbf8fd863d32bfb7e038c5ee49a53a243b7c4b619f8f01f2528b63d3d30ee42353e63991460d3017d690d5d1d4061308bcc06d6df4b637605a12c71871455de0ec0ca9d4bc0618c739d4a1d2840b40e941da2f979eecb008f4d42936fd507dd2ae990e56a60d0eb73c942acc11a32316852505bc07ab49bfdfbdee30b32738060a00deeb7a72fcecaca2c2fb0c78d2572b23d95a996fbae088c4089a1c7d78a6879e40b1d8a08912c83a221c96cdcf3c27a041039e0c082b49aa120a8612ebbb8b212643e5f919598c6d783c944ae489e397c5dd3ea0afd730a6bd1c439380d86e05420a08145ffff0b1c76da1f920b39ff18cb04a3dc17c98b841c7bbff185d647e3b73f411f897ba7012863e6c3f58d734e3e105ff9e66cf6fa967763ff752823c056262bac4ca4ec4cdea379a6021f4674f16e4897b09752a737058c6539bb958782e5e2a9c87fa86b281e4fcc52acb75d568f6bcaff64d47e2cd235efdf5d1587ba860af4a62d1b04809f8fb934412ae24a9ac4dae0f8b9b6984b08e016c7053aa46fb312790aa0aaf31e7996fbae29c724027a0309818b8abc4baf5c5d96a582f55b79cccea28bcdf5ee03001dfff5cd945dfa476cd9008aea7eea2362719bb57342e15719a80d95056f0db56af34639b334309caddc833513c772ba05b7a365d6e91141112e6570296fce32640717602308ca37497d9efbdf284d2f4e7502ef3072a9e01f4a9a055457ce9db2b66c6e20aac758aaa1d085a1143bafe1ca3bb961379a02718f0fbbc7394f89b6ed096317866c50eb17fbb56ef9e0ef4ba22eed07a2cfa955669b7861ff662fe5698eb2d6e608a209c2286f0a6ab0cea4db0a567747498be3aac058521cb00e1ad7c6b6223f01f755c9ea146ec5c36562116172684cb16ce4479da96c83f6f509002ce583dfaa127db607a494aa3676db46be3e5faf22a990280626e7e39de54b6437302e194aa3ccbda92483130976b902cb621226639664deb1564336ae7f7d60d8c81ede66a154b38c147ce6c8ddfc93e8c20116085603c2d8cbeb6eed491e82d1838809bba6ff51bf56155c1e3e5eb4811bfeb2f68c17e4830a4fdaae17bdc1ff6869aee810941f3a7d3e4b01b47a118bf44f98d0acc3580390a05a4597f3a055498485d7850ef0a30cc412156e872a8ac1525f00c09113370c97ee408b6669998c6bd4513dc1ed023093e93354650c5c9be1c302a3e6f4ea612c6f24b2f1d31344d0f8ba7715ce0d0ca3ec816938fc2b7355c3424a2d2af92c6ed135bb8a0f2e692dfac2a25ca123dac083ad7f379d9316d320da4d4f6df987064e5918b5b55d0e39d97a1e972097f3cc790edf8479aa57d1dc911cb1c584aabef898354e0858b7c869ea0f3b4a827bc681be89ede0af01dcd2856595eaccb0d6ffd4aefaf3c1d406e39faba62ea6d46d24000097db353f506c4054e9309827a32246d6db154689bd3d164e9ad0ee9e662cb4f66e4f191d2adab221a004f49a035ab639d3a7a9044a148b12c4d0809dbe51231938026da42e017e475eca0d6f6f584dae896b73c5c1048ef04d42389728b8ad82479de84600161a731949cf60a2b18093ce01b865c31cb192dcfa58942f15ce2396893408b81b69b02d146412f8c5c57962aef640b774098da0b21442042325efa87ec757e45b40d0b01555296a39172f3f72098c6564a7a40b5526b63208f411e01fe949af04904c39b78775cffca74cd36af636971621ea74c83ad29f2cc55974c5ba7fdfc4e3b743f906e9d739a545f5974bf44e2f695326a2296fc4eba138971507b5c208e78fbaa94a1f0f6332ff6f242679dca0034472a9ac61f993a8f09dae64b860a44aa5e61c527122e4074a24fae53f58b713df614d5a308c91299f13f34ccefb9c4aab4f75baec8d0ac16d8e79115ebb45499a39d5ff53b40eb21496ebbb37c9a691748b80dd24b9d6f869300799059f284c39b97f1f75b773d78793de665ad8af00dddbad2208b6b3add604e1fa2d567f44be4661d4e74d72fb2018dc31a6542e856c9ca57804cbc9b5af7e14ffbc1fc59a65e845ecbdbb8cf1a8797224dfa0c76e8f995f899d541d50dd45cab0f05312ab8ea7a8522a1756d6d2e770d93500f7782670068603ab43ad972b4a55b42e5e0932790eba71343d1da3dfe2490959a3c11c2a1e859134047d18fbf75445d34cbaf139550fe0d9a72f80e13310255cc82b955156746e04272811b74a0b3ca3c2866b59b12dda698d562a619238965ed1200b8b2f41d3e06318067a0c248112a3aaf3aa30a396be7422986e35b41e8d375f76010785ff3eb09489b70e5a2c82984a10812bf9b1c7f4f877df41bab0d7b01e685670c56f3e545329a4a882fb0a7264343d54597346b15df13f25d7a0931a887a7be4465b717ee4552a0192534a10b2d19d086d577d1adb8154787e3af1050370e60599d3636e0d5081e357c5483e2a5eafb2f71c6db5b1213f4a07ffbe2a86eec3960d242fe2106ffbd9865882ae4dfc2d9542cda9a26a3320e72db1fe68c1235c468bffdb20a26791df145dac5a0d2c9527ea841961c45fda95d3d0267955dfef6bd960231c1a8dfd36583533836a1dd94d4b456a97ef41237fd6d8d5b898aa21eed123218ef4b3aedd3e645c9c0166c26e652699bf951f8f6f0af9ae11500000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7dd123", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd5c156a48ee21fc31fd16f0102ba7f09ae50f5f040c95fec880ebd46b96c2506", + "transactionPosition": 82 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b1e7f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4d9396e", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002b1e7f1958b3811a045b26ab5820f8ccb64bff08817a3cb554d2b97d7ad9fb9428ac6bde0bd9144cfdffec760af75820749b5789aaaaa013a8a90db28d47429b37b0c322abf2f2af8eb0261f3fd9f133590780b104649bcc2962b63068e99c9027750b8c0624579c9782df07b725bbf8fd863d32bfb7e038c5ee49a53a243b7c4b619f8f01f2528b63d3d30ee42353e63991460d3017d690d5d1d4061308bcc06d6df4b637605a12c71871455de0ec0ca9d4bc0618c739d4a1d2840b40e941da2f979eecb008f4d42936fd507dd2ae990e56a60d0eb73c942acc11a32316852505bc07ab49bfdfbdee30b32738060a00deeb7a72fcecaca2c2fb0c78d2572b23d95a996fbae088c4089a1c7d78a6879e40b1d8a08912c83a221c96cdcf3c27a041039e0c082b49aa120a8612ebbb8b212643e5f919598c6d783c944ae489e397c5dd3ea0afd730a6bd1c439380d86e05420a08145ffff0b1c76da1f920b39ff18cb04a3dc17c98b841c7bbff185d647e3b73f411f897ba7012863e6c3f58d734e3e105ff9e66cf6fa967763ff752823c056262bac4ca4ec4cdea379a6021f4674f16e4897b09752a737058c6539bb958782e5e2a9c87fa86b281e4fcc52acb75d568f6bcaff64d47e2cd235efdf5d1587ba860af4a62d1b04809f8fb934412ae24a9ac4dae0f8b9b6984b08e016c7053aa46fb312790aa0aaf31e7996fbae29c724027a0309818b8abc4baf5c5d96a582f55b79cccea28bcdf5ee03001dfff5cd945dfa476cd9008aea7eea2362719bb57342e15719a80d95056f0db56af34639b334309caddc833513c772ba05b7a365d6e91141112e6570296fce32640717602308ca37497d9efbdf284d2f4e7502ef3072a9e01f4a9a055457ce9db2b66c6e20aac758aaa1d085a1143bafe1ca3bb961379a02718f0fbbc7394f89b6ed096317866c50eb17fbb56ef9e0ef4ba22eed07a2cfa955669b7861ff662fe5698eb2d6e608a209c2286f0a6ab0cea4db0a567747498be3aac058521cb00e1ad7c6b6223f01f755c9ea146ec5c36562116172684cb16ce4479da96c83f6f509002ce583dfaa127db607a494aa3676db46be3e5faf22a990280626e7e39de54b6437302e194aa3ccbda92483130976b902cb621226639664deb1564336ae7f7d60d8c81ede66a154b38c147ce6c8ddfc93e8c20116085603c2d8cbeb6eed491e82d1838809bba6ff51bf56155c1e3e5eb4811bfeb2f68c17e4830a4fdaae17bdc1ff6869aee810941f3a7d3e4b01b47a118bf44f98d0acc3580390a05a4597f3a055498485d7850ef0a30cc412156e872a8ac1525f00c09113370c97ee408b6669998c6bd4513dc1ed023093e93354650c5c9be1c302a3e6f4ea612c6f24b2f1d31344d0f8ba7715ce0d0ca3ec816938fc2b7355c3424a2d2af92c6ed135bb8a0f2e692dfac2a25ca123dac083ad7f379d9316d320da4d4f6df987064e5918b5b55d0e39d97a1e972097f3cc790edf8479aa57d1dc911cb1c584aabef898354e0858b7c869ea0f3b4a827bc681be89ede0af01dcd2856595eaccb0d6ffd4aefaf3c1d406e39faba62ea6d46d24000097db353f506c4054e9309827a32246d6db154689bd3d164e9ad0ee9e662cb4f66e4f191d2adab221a004f49a035ab639d3a7a9044a148b12c4d0809dbe51231938026da42e017e475eca0d6f6f584dae896b73c5c1048ef04d42389728b8ad82479de84600161a731949cf60a2b18093ce01b865c31cb192dcfa58942f15ce2396893408b81b69b02d146412f8c5c57962aef640b774098da0b21442042325efa87ec757e45b40d0b01555296a39172f3f72098c6564a7a40b5526b63208f411e01fe949af04904c39b78775cffca74cd36af636971621ea74c83ad29f2cc55974c5ba7fdfc4e3b743f906e9d739a545f5974bf44e2f695326a2296fc4eba138971507b5c208e78fbaa94a1f0f6332ff6f242679dca0034472a9ac61f993a8f09dae64b860a44aa5e61c527122e4074a24fae53f58b713df614d5a308c91299f13f34ccefb9c4aab4f75baec8d0ac16d8e79115ebb45499a39d5ff53b40eb21496ebbb37c9a691748b80dd24b9d6f869300799059f284c39b97f1f75b773d78793de665ad8af00dddbad2208b6b3add604e1fa2d567f44be4661d4e74d72fb2018dc31a6542e856c9ca57804cbc9b5af7e14ffbc1fc59a65e845ecbdbb8cf1a8797224dfa0c76e8f995f899d541d50dd45cab0f05312ab8ea7a8522a1756d6d2e770d93500f7782670068603ab43ad972b4a55b42e5e0932790eba71343d1da3dfe2490959a3c11c2a1e859134047d18fbf75445d34cbaf139550fe0d9a72f80e13310255cc82b955156746e04272811b74a0b3ca3c2866b59b12dda698d562a619238965ed1200b8b2f41d3e06318067a0c248112a3aaf3aa30a396be7422986e35b41e8d375f76010785ff3eb09489b70e5a2c82984a10812bf9b1c7f4f877df41bab0d7b01e685670c56f3e545329a4a882fb0a7264343d54597346b15df13f25d7a0931a887a7be4465b717ee4552a0192534a10b2d19d086d577d1adb8154787e3af1050370e60599d3636e0d5081e357c5483e2a5eafb2f71c6db5b1213f4a07ffbe2a86eec3960d242fe2106ffbd9865882ae4dfc2d9542cda9a26a3320e72db1fe68c1235c468bffdb20a26791df145dac5a0d2c9527ea841961c45fda95d3d0267955dfef6bd960231c1a8dfd36583533836a1dd94d4b456a97ef41237fd6d8d5b898aa21eed123218ef4b3aedd3e645c9c0166c26e652699bf951f8f6f0af9ae115d82a5829000182e20381e80220f8558d739d8803bc6cebfb9d848e53a6dad4c37fab064e8c705621641b35074ed82a5828000181e20392202061158a40f8ea8f3f04148354f737e70549998404cdc66a5583c2f4e3cd0a0a1d000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x30cf1b6", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd5c156a48ee21fc31fd16f0102ba7f09ae50f5f040c95fec880ebd46b96c2506", + "transactionPosition": 82 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ae495", + "to": "0xff000000000000000000000000000000002b1e7f", + "gas": "0x402499b", + "value": "0x105e0cd9863c06ef", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821958cd590780adcd547b9f1054f3eb179481e1437f981389c937872090f003ddb6e08d753861e6f05e90b9990d98c754dee7d6dddcf1941a48d0ee5bc3570fd52520a5ce3d9da7b40f68761b3cf3a7633dde498283d95055e3e32521d8a720d41321026044280910a2478ff76ce23192d695ab518438f967da0da1912608ad89ea0a50d2702b5032f026c62cce894480256c74c639bab56435da3aefad1ad28659d132ecbce88d3f80b0cc4a3048e94f13803b84404f17dc0a2d57b81ccb1255eadf2a55d5fa892eb861314ca95a1d516f63df0bc93c77529573929e10aa35c8e0ba9c225df897ff0d005be902a2d74c4a693893015f8f14311232d816d1fad5a4e005e685d426dd78c531e9563dd8785ac95e44970fac7f3b7d8d6603329604061b626cae7502fbac2ef0c460af26be45e113b2b7a43d935116511231a1c33ac5d769ef3b0c9c3fc626fbec2f8cfb87572e68c2d617897a9fe18d703b26f4253e931ea3e9c1a4b5077fc7656797b8df389b14bb5e2888a7dcdfdf89dcbc81c34d236d31f021a7a207dc806af6f3c89889efedd24f511562a2d01bea7b55afdea26e486bb729cab9b33a63c3360f8d9f267673eee7aab384b4d6e92d99955b0f1d58dce11f6e41dfe0ad020db66d34b9809c01d2f652a567b0ee46bc2ea4743a3f570d96acbc1525a2691d8bc7e5a1abd34f91189f9aacf36cb833f75be11cf12d47a985667f8b65c5444b1995495ea618484846e5038a8bff82cdb012ffb11bfb14934cc26e4e00331511d49ab2634d2d521a0989aca51ba718f0247dbdb88693f094bbb045a4d9f586340fc388fb9b4e0c65b52a38287cb8614bdad6271c3122e845ce8e74fef8bf6a5764d4384c1273fe5ce3037a951b46409d1518c256e26776e58f7f54f710ee3f190b4706de47e1c4dc0f427fc026a55c4b78deda2aebc381b98de5e200a765da1713999ca2a0d4de1ae76f12442e43e367055cb854ca45aed1115a0e9b20aa1fd188613f7c75beb1a2d26617890e786f2bdbab208b9dad19c1e6ec5dd69b6bc65657fbe561f9efae97eda5d4d855841f450d60fdceeddb45f371b5ed86ddf2cd367ad9a977eb4d9161ee549f5ab243513b70fe3f5708d4f13a47731db9898777079601f103ad01acb74075eda60036ae233fc572e41e546081f852496dad5e90ee06d722cd3835294cda3a31b61a3a167138d00aed08ebbe66a970e80334142cfdc870855c9b6d32e5942d526ed4762a13f9372572cb2555bb3aef5428906719050e1aa6a16ebecd5b35a38f940b3d4b12308a155e5bf035484bc1c2955ca84c23c25801065f7bd907b43e98116262a29c52d313f5355adf15b98f7e9038d780a2ef90049625360d7aa4ad4db577bfd9258d5627932993250fbc0b10b037a899612b74ac70c044044ca1e2c4a4cabe88260646811723ec43609158519e563e44a34ac6ca462c1cc2e6d79b48ed604e5fb890f41f0239ee947dcf6daa11da281778f48e78f9280f54d54c728c2ec8299e710c6c3acda600ec4b55e421c4d77376d8da43f1eb65c473c00f08c3ae15348d1049f75449826c0f9ee69361241491d8261d82daded638e80e042cea44cd702a529329aa603704231b24e933aa6d700662ec989717c7bd430cd3023a53221052ec6c3539c2e15518dc75f6118da892225026bc05c0802b106e6240d28505cd2a4ba725484ca570ed17c7a254e7a338aefd50ae4f45e3bf2d8ffd4695ac84bda612cd4fbc6dd857735594ac3e0457fd9c7e650c66fc77b562c31ef1ce72e50efc6c2ff3fabfd59c601e98a35fbc269d02a7c274f6d814b167217d3053a50fb5181bfe154ccef4a6f83cdffc84d6483072335302dc9210f0370fb49d01fa9675c9952c660f53b0def14bb37043a979df12da6ee45e3ee721b8364c64f9bf80d3b53c67f4888d1b22defc537c0d50cb0fa1c0074ed63333d8e1095b945f902f303a784d070ab17b68765f138598501387130ab052f0056f3881d376b6eef1b256af53d985ae9e8c2613d6859acb171bd1101c29a5b3319eb0b97459948ff358d596fb1a08388368bc9174756855b3b1a629706bf31d16e9dccb3e05b57581da42995a170f8b77103c24ab1e11f287c7dd1c089238da7c24411148130654996d194c5bcab051a092be26332a16b195248002370eb07a43411aad2128e64c0ca0bbda8bb6b25e87628d8bd55c28666bfb999afa7e0dda689c8fb0d7059f418b2af12a70d310c2bb564551159e52f34ccf38f40ec3d9661b57a91c6f8d84a11f86058b4ca466ec86ccd32ac4d69aa0057d65f04dc6f6da6daf63db98605f948b9b9f191eb9a2fb7b4c6c669f446f7fd47300257a50cdfe05030011def8e0d493254dedb9e343a6e0209b91cd3237e2c6d2ae7f36ecfd5aea18d3a6f65568875e0f05b678701f09de308cdc203112568204e2bfe4dbaac2b2b6fd636c96230c2748e8d111252ce1dd1f208c26528d7df70c37546bbb173a609e591782252adc8245049da8326c3d4c5bb9fd4956de0c1204a9c88987c1a3df6973fb1aa9efdb3d9d6ec7aa69e066487eb29edf789e510e3d454eee28598dee5a505644cbc372d039c5ee473eab761ae5ad64a40e389bd65433d15e42de02d7f1bfb64f5d392999f48c0eb5a1d16a0e31453fe75db5aa2992f395d24f0364653bf9c19dedf45ddc3fd0f13454973785990d18c1d6ae2000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7bf31d", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xfd6e6033273714f103cb791d301d0cc728ec726a9ed10ac961ea47833131ab2b", + "transactionPosition": 83 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b1e7f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3b7567a", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002b1e7f1958cd811a045b259d582042b3e721f3f06ce0795d00e70534f16e2e39d65a22e99601ea49474aee1c6ff65820b9d0d8ad40b13f20a3f521674d65d23f718195e3009e84f1a2b06f61fe851092590780adcd547b9f1054f3eb179481e1437f981389c937872090f003ddb6e08d753861e6f05e90b9990d98c754dee7d6dddcf1941a48d0ee5bc3570fd52520a5ce3d9da7b40f68761b3cf3a7633dde498283d95055e3e32521d8a720d41321026044280910a2478ff76ce23192d695ab518438f967da0da1912608ad89ea0a50d2702b5032f026c62cce894480256c74c639bab56435da3aefad1ad28659d132ecbce88d3f80b0cc4a3048e94f13803b84404f17dc0a2d57b81ccb1255eadf2a55d5fa892eb861314ca95a1d516f63df0bc93c77529573929e10aa35c8e0ba9c225df897ff0d005be902a2d74c4a693893015f8f14311232d816d1fad5a4e005e685d426dd78c531e9563dd8785ac95e44970fac7f3b7d8d6603329604061b626cae7502fbac2ef0c460af26be45e113b2b7a43d935116511231a1c33ac5d769ef3b0c9c3fc626fbec2f8cfb87572e68c2d617897a9fe18d703b26f4253e931ea3e9c1a4b5077fc7656797b8df389b14bb5e2888a7dcdfdf89dcbc81c34d236d31f021a7a207dc806af6f3c89889efedd24f511562a2d01bea7b55afdea26e486bb729cab9b33a63c3360f8d9f267673eee7aab384b4d6e92d99955b0f1d58dce11f6e41dfe0ad020db66d34b9809c01d2f652a567b0ee46bc2ea4743a3f570d96acbc1525a2691d8bc7e5a1abd34f91189f9aacf36cb833f75be11cf12d47a985667f8b65c5444b1995495ea618484846e5038a8bff82cdb012ffb11bfb14934cc26e4e00331511d49ab2634d2d521a0989aca51ba718f0247dbdb88693f094bbb045a4d9f586340fc388fb9b4e0c65b52a38287cb8614bdad6271c3122e845ce8e74fef8bf6a5764d4384c1273fe5ce3037a951b46409d1518c256e26776e58f7f54f710ee3f190b4706de47e1c4dc0f427fc026a55c4b78deda2aebc381b98de5e200a765da1713999ca2a0d4de1ae76f12442e43e367055cb854ca45aed1115a0e9b20aa1fd188613f7c75beb1a2d26617890e786f2bdbab208b9dad19c1e6ec5dd69b6bc65657fbe561f9efae97eda5d4d855841f450d60fdceeddb45f371b5ed86ddf2cd367ad9a977eb4d9161ee549f5ab243513b70fe3f5708d4f13a47731db9898777079601f103ad01acb74075eda60036ae233fc572e41e546081f852496dad5e90ee06d722cd3835294cda3a31b61a3a167138d00aed08ebbe66a970e80334142cfdc870855c9b6d32e5942d526ed4762a13f9372572cb2555bb3aef5428906719050e1aa6a16ebecd5b35a38f940b3d4b12308a155e5bf035484bc1c2955ca84c23c25801065f7bd907b43e98116262a29c52d313f5355adf15b98f7e9038d780a2ef90049625360d7aa4ad4db577bfd9258d5627932993250fbc0b10b037a899612b74ac70c044044ca1e2c4a4cabe88260646811723ec43609158519e563e44a34ac6ca462c1cc2e6d79b48ed604e5fb890f41f0239ee947dcf6daa11da281778f48e78f9280f54d54c728c2ec8299e710c6c3acda600ec4b55e421c4d77376d8da43f1eb65c473c00f08c3ae15348d1049f75449826c0f9ee69361241491d8261d82daded638e80e042cea44cd702a529329aa603704231b24e933aa6d700662ec989717c7bd430cd3023a53221052ec6c3539c2e15518dc75f6118da892225026bc05c0802b106e6240d28505cd2a4ba725484ca570ed17c7a254e7a338aefd50ae4f45e3bf2d8ffd4695ac84bda612cd4fbc6dd857735594ac3e0457fd9c7e650c66fc77b562c31ef1ce72e50efc6c2ff3fabfd59c601e98a35fbc269d02a7c274f6d814b167217d3053a50fb5181bfe154ccef4a6f83cdffc84d6483072335302dc9210f0370fb49d01fa9675c9952c660f53b0def14bb37043a979df12da6ee45e3ee721b8364c64f9bf80d3b53c67f4888d1b22defc537c0d50cb0fa1c0074ed63333d8e1095b945f902f303a784d070ab17b68765f138598501387130ab052f0056f3881d376b6eef1b256af53d985ae9e8c2613d6859acb171bd1101c29a5b3319eb0b97459948ff358d596fb1a08388368bc9174756855b3b1a629706bf31d16e9dccb3e05b57581da42995a170f8b77103c24ab1e11f287c7dd1c089238da7c24411148130654996d194c5bcab051a092be26332a16b195248002370eb07a43411aad2128e64c0ca0bbda8bb6b25e87628d8bd55c28666bfb999afa7e0dda689c8fb0d7059f418b2af12a70d310c2bb564551159e52f34ccf38f40ec3d9661b57a91c6f8d84a11f86058b4ca466ec86ccd32ac4d69aa0057d65f04dc6f6da6daf63db98605f948b9b9f191eb9a2fb7b4c6c669f446f7fd47300257a50cdfe05030011def8e0d493254dedb9e343a6e0209b91cd3237e2c6d2ae7f36ecfd5aea18d3a6f65568875e0f05b678701f09de308cdc203112568204e2bfe4dbaac2b2b6fd636c96230c2748e8d111252ce1dd1f208c26528d7df70c37546bbb173a609e591782252adc8245049da8326c3d4c5bb9fd4956de0c1204a9c88987c1a3df6973fb1aa9efdb3d9d6ec7aa69e066487eb29edf789e510e3d454eee28598dee5a505644cbc372d039c5ee473eab761ae5ad64a40e389bd65433d15e42de02d7f1bfb64f5d392999f48c0eb5a1d16a0e31453fe75db5aa2992f395d24f0364653bf9c19dedf45ddc3fd0f13454973785990d18c1d6ae20d82a5829000182e20381e80220a73bc120d1ef9c13aff71a3ca6e094ceab44768e7463c06c5411d342fa01ea2fd82a5828000181e203922020fcf4f8a87325eae4550a7b7833830414cfa062bec0a61e0de1981315771db603000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x382d430", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xfd6e6033273714f103cb791d301d0cc728ec726a9ed10ac961ea47833131ab2b", + "transactionPosition": 83 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc31b", + "to": "0xff000000000000000000000000000000002cc320", + "gas": "0x30439df", + "value": "0x8abb7a55e7bee0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708199e96d82a5829000182e20381e8022012bebfcae441d607d18402467f62c2ab7f7817897031757fc1e3804dc04442311a0037df6b811a045b14c01a004f9a76d82a5828000181e20392202055b250e600ac801d6356d82b0df8a453360fb0ec7084b1910e2759234e15902600000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x20d62cd", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf56db0f8b6c0adf7d4b97adb155081e3d4159fb405b85419eb6d94d7d2452c78", + "transactionPosition": 84 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc320", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x2f17cd0", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf56db0f8b6c0adf7d4b97adb155081e3d4159fb405b85419eb6d94d7d2452c78", + "transactionPosition": 84 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc320", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x2e0b788", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf56db0f8b6c0adf7d4b97adb155081e3d4159fb405b85419eb6d94d7d2452c78", + "transactionPosition": 84 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc320", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2cea217", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f9a76811a045b14c00000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x476739", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e20392202055b250e600ac801d6356d82b0df8a453360fb0ec7084b1910e2759234e159026000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf56db0f8b6c0adf7d4b97adb155081e3d4159fb405b85419eb6d94d7d2452c78", + "transactionPosition": 84 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000241c2e", + "to": "0xff00000000000000000000000000000000241c3f", + "gas": "0x48d6a79", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000072818187081a00010a76d82a5829000182e20381e80220c68a4c0a1f9bbacc1b43b94dd70a03f0360cf525da916df0d7021b78895cf1501a0037e0a1811a045b34251a004f93c3d82a5828000181e203922020ee6562b75f62cccc594244cb2f9eb26aca725a060f7eba8ee3c4b32e418c2b230000000000000000000000000000" + }, + "result": { + "gasUsed": "0x347e2a4", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd8ee13b065e0b4ca8217629d5ad2cce9dd7000642534213e4531e1b3f8a1da29", + "transactionPosition": 85 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000241c3f", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x4820429", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd8ee13b065e0b4ca8217629d5ad2cce9dd7000642534213e4531e1b3f8a1da29", + "transactionPosition": 85 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000241c3f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4713ee1", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd8ee13b065e0b4ca8217629d5ad2cce9dd7000642534213e4531e1b3f8a1da29", + "transactionPosition": 85 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000241c3f", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x45f2970", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f93c3811a045b34250000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x477613", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020ee6562b75f62cccc594244cb2f9eb26aca725a060f7eba8ee3c4b32e418c2b23000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd8ee13b065e0b4ca8217629d5ad2cce9dd7000642534213e4531e1b3f8a1da29", + "transactionPosition": 85 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce389", + "to": "0xff000000000000000000000000000000002ce3be", + "gas": "0x31bd06d", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708193f04d82a5829000182e20381e80220ee335a22ee6fe624ad6988dc7323bee4e562b9be560510700bf6d6a46914ca021a0037e0f1811a045361b11a00480bc5d82a5828000181e20392202088a1d5a6621a6fe38348e63ea64d091d8ab3060ee1769ad08c5e608db0ce4b3200000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x21e08e5", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x29a86bf568ea52bc778da4c2f1882f2b2459e8d5e638294f5b3c7341f39452a6", + "transactionPosition": 86 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x3106a46", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x29a86bf568ea52bc778da4c2f1882f2b2459e8d5e638294f5b3c7341f39452a6", + "transactionPosition": 86 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x2ffa4fe", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x29a86bf568ea52bc778da4c2f1882f2b2459e8d5e638294f5b3c7341f39452a6", + "transactionPosition": 86 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x2ed8f8d", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a00480bc5811a045361b10000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x4887a6", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e20392202088a1d5a6621a6fe38348e63ea64d091d8ab3060ee1769ad08c5e608db0ce4b32000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x29a86bf568ea52bc778da4c2f1882f2b2459e8d5e638294f5b3c7341f39452a6", + "transactionPosition": 86 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000000040f", + "to": "0xff00000000000000000000000000000000000961", + "gas": "0x2223d42", + "value": "0x8abb87dff2adb5", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000708181870819b878d82a5829000182e20381e802204b991204e7215867548c947fe66e43215213e86fc030522fb3f35f9cd9a6e2311a0037df12811a045b0ded1a0047eca7d82a5828000181e203922020864c24ea0ee60743452cd1444db396bb8e7d84b77867be2b737c1cc7b3fc7e0c00000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x1588de7", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x21e6ee3748a0d4cbe0125a25f22d5f2611d4c3df2f3aa4296736a8470b3ba6da", + "transactionPosition": 87 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000961", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x20f8033", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x21e6ee3748a0d4cbe0125a25f22d5f2611d4c3df2f3aa4296736a8470b3ba6da", + "transactionPosition": 87 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000961", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x1febaeb", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x21e6ee3748a0d4cbe0125a25f22d5f2611d4c3df2f3aa4296736a8470b3ba6da", + "transactionPosition": 87 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000961", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x1eca57a", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a0047eca7811a045b0ded0000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x477494", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020864c24ea0ee60743452cd1444db396bb8e7d84b77867be2b737c1cc7b3fc7e0c000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x21e6ee3748a0d4cbe0125a25f22d5f2611d4c3df2f3aa4296736a8470b3ba6da", + "transactionPosition": 87 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c43aa", + "to": "0xff000000000000000000000000000000002c43b6", + "gas": "0x413ba3c", + "value": "0x1a86cf1d84124d89", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000787821925b959078097b3ea67b0001eb0ba6353d1eab30d9a7d5f9d64b1fc95a1ba6bbe5f11044aba6e3b40ec1fbaa7166ffc8c64971a27daab26e31ce688f7e1f24885ee59ecc8677d59583424689338d48829a9e4d58d2e906dd592bd26d202a5759808a1acc7a6063ece776d981d3d8444a369728d65d875a3666e804785f65460925fcae2cad1defebb202b02eda7b981f6430479db06ac90441ffaefd375988c308c894388c83db61e80aa53106012c61a3ba70d4eab30435d7186a9f6c536c556a1c994e11cb4fd170312132b5c59b29c3f6b4b1188dc51c92352419f4504d3122eabd074144ecfcaff2dfa6c27781368afb9f0a49f872f659845c00949f638c96e473010286922edbe98e8db6132594fe9f487f7fb66bdd7f9a27b7813c1f29be7cd833d9711d2d04bac97f5ef5598f73110020987ae2da8f2db9befd50a033cd74c2d878d12f9b69b5d7e3192a76362ddad3c8f00b2da963f9c44818ebde797a3343b196751522f4de82fded7fc43d2ceb05b143c103821d56694a5f7f01d3678affbd8c58b110c139d7c49442a2e591d8edd45e9b9d62975ff4fa4c2f75fd4073dc6647ead728e690971b348c1dae698a402a0d697ec1aad2b423077d9cc55b913d016b217d90d21f310e4342b5fe901f492aa7d093a2e4567bdf41a4d9e86cd7242467c1003908559f717a2ab1e4e2baf7311364544229550a8ba213596202efd70cac595c51408723a68bb0aa5984e32dcf3bba7328dbc7ad8dc0b8650cc9995bb99eef736fb4a5f8929e66b787ec028fec84d7487e3125220ec2db0f96b85314be66b88cdf061dc2b5a1442ff6341a038fb1118f81fb2718031634313e65516b8fe58643d215778c7c6afae8d5d5c569d3726b757cc558705e99d76eab551b2fe464c4a79e432ad4eefd478646f6f61fe1613ecf973d42466c1b3dec9d6e8cbc1681808ae1bb85bdacc9f8cb060ed9fba00fba8694b1dbd950ab5e4a597058faa19e77990be62cbba63f476a752bfe48aaac4a5ff8bc85b1b0c1f0fe9b1caa247196c3394c8f0f8f13708b77213155d5614f5bf7af0b9c9f574717b5a5d72f4f7367aa0593563e9be8c5f2d4c3f2096d1f3c9d82bfec6b8ce2edf5aeaab4fba6d9b72e95c9638ae856af76fe598b088f6ddabaf8c1aa1af5137cbf681819f4e4db91ae336431e7c1c5363673ea9b332393003e031ae9bf4b20dd5a4a7859365ccedb10c2e2e226e1684f249afca9268c23352fb36e9ebd18cf254b9e6c70268275c5b83ff8b30c7abea185f4969bb3673330d959675f76b6bd839620c8dec153d4a7d79d012d994211acefeebf8b15f0f07fd7a4420e5328f7ec096d3292574d492698fb159987beb422ec5c620a211bdff153e57f567ac37843ca7dc3844961836f78ef5f590d9d1d8c29c9797060b2386dcb0b3d02b7ffe3b2e25cb45f4ba0de07b71a9864e7e0a2dd895bf642f49954ca2a7f95007a2ffb7dfa570c6e124fe007e06d4b66aa8ce81f550ec3c1cc4ce9186425b348d191232c76cb4018703b42495f28e113c80411566c85d61523f71aa08976c181005d30b306168d2c5c88ebdb951603f190dfd3fe48ede2858e02a77b90fcdbdcc34fa5098c9f3cf6142be856da830f43d8ab50b990c29e4a957241946974fb63f8da961b82b11ad4034e90528548659e5aaf3156dbbfbfa8d83c3e07981758442aa3be462c5983908a882ed8fb298e3c6bce51db1fa61c0faed67f7014d9f7fab463b088ec32aad30c680f463009f785e2f74003a90bcc05c4dedc376a11daca09891c2cee6242d8ace7743525c6b8988b41612fdbeea48098605b597b4cf3b736205688e3c1cfb2c5d8e9a70f805014159b9bc02a6baf2cf5daeec39bd1057343572c776f66be721e139ac74a30f6630e165d498640b4a3074689e7ee7bb04a8d0611b9323b4141195d94b26d4147ae734f3002c644c671f8c971e539323ce50fc44b6e045faafd3dac798d67cbb4268707aa9ea783eedf2300989fce3e2a519775bdfb01a25ba57ec7d608700fed67cc95b4cad48e9301edbb790e79de8acdac2a6cf2d3201251e6931b2ac655dcab2a41a761082a3b3d565b27455a4a278f70b58ae17cba43cbb7865fa9915a5129d9e20f7ffbb8a46e7861c85da4d780a5a0af6b7396c7a64ba30023ad9871671c13a682b600f9c64894524ce34dc8ce1509e8cc5ad34e899833aa8f67933d2f6521e9c425f77e8a978bc052aba8c8d23c30dfb1dc40ada63d5743705b8a6dc053e918d8b5e7f6840b2d9810900ed076c6cdcf7e143d8a6a759a9dde1f10b714fa57d2e87350a9d3c57f973a9db4576f68703b901938504a87267da534c2b6f6bb517c229002df0a23cc7dbebf1b81e8412c13786ce60cff177b940a76e5b499de0d8cf8457ae43ae9908f3ae0cc22b9778ec04404efbf93afce4c8f015894d3e4ae82d11f61d41ad02e0be7cf7c8639b8d280dc152fd75e9d0efcbdb9b90502d757f7a2d386f8bd001a95753bf81308b1dc88da58b1f221dd4e8e5217345b7cb0b6fe940874920af33481091a1e3ed6024afab00eb8cd3e15e142d9b5e1446a4554923ce97cb385867786e95304fd8c62dc0de08ba942d7d7493120b4b4a54b379594dc0558381305d6fba675582f6d7761513470ffeb5de56e9448922b452ec437704f08699c4831684239f07162b6debfe74f2dd61ac707cb98aec2a00000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x62aed2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf9b127b6341a6e269f846e39956e38fe62f80c4ba8af19158913490d0d87aca3", + "transactionPosition": 88 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002c43b6", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3e21973", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002c43b61925b9811a045b1f095820834b0ec1478d47321f74ea8410e823e1b5849bad4ad3f9e99398927317394e795820a6370784af85f07361123ee661399b72bef32bebedf8edc99da47959d5e2c81b59078097b3ea67b0001eb0ba6353d1eab30d9a7d5f9d64b1fc95a1ba6bbe5f11044aba6e3b40ec1fbaa7166ffc8c64971a27daab26e31ce688f7e1f24885ee59ecc8677d59583424689338d48829a9e4d58d2e906dd592bd26d202a5759808a1acc7a6063ece776d981d3d8444a369728d65d875a3666e804785f65460925fcae2cad1defebb202b02eda7b981f6430479db06ac90441ffaefd375988c308c894388c83db61e80aa53106012c61a3ba70d4eab30435d7186a9f6c536c556a1c994e11cb4fd170312132b5c59b29c3f6b4b1188dc51c92352419f4504d3122eabd074144ecfcaff2dfa6c27781368afb9f0a49f872f659845c00949f638c96e473010286922edbe98e8db6132594fe9f487f7fb66bdd7f9a27b7813c1f29be7cd833d9711d2d04bac97f5ef5598f73110020987ae2da8f2db9befd50a033cd74c2d878d12f9b69b5d7e3192a76362ddad3c8f00b2da963f9c44818ebde797a3343b196751522f4de82fded7fc43d2ceb05b143c103821d56694a5f7f01d3678affbd8c58b110c139d7c49442a2e591d8edd45e9b9d62975ff4fa4c2f75fd4073dc6647ead728e690971b348c1dae698a402a0d697ec1aad2b423077d9cc55b913d016b217d90d21f310e4342b5fe901f492aa7d093a2e4567bdf41a4d9e86cd7242467c1003908559f717a2ab1e4e2baf7311364544229550a8ba213596202efd70cac595c51408723a68bb0aa5984e32dcf3bba7328dbc7ad8dc0b8650cc9995bb99eef736fb4a5f8929e66b787ec028fec84d7487e3125220ec2db0f96b85314be66b88cdf061dc2b5a1442ff6341a038fb1118f81fb2718031634313e65516b8fe58643d215778c7c6afae8d5d5c569d3726b757cc558705e99d76eab551b2fe464c4a79e432ad4eefd478646f6f61fe1613ecf973d42466c1b3dec9d6e8cbc1681808ae1bb85bdacc9f8cb060ed9fba00fba8694b1dbd950ab5e4a597058faa19e77990be62cbba63f476a752bfe48aaac4a5ff8bc85b1b0c1f0fe9b1caa247196c3394c8f0f8f13708b77213155d5614f5bf7af0b9c9f574717b5a5d72f4f7367aa0593563e9be8c5f2d4c3f2096d1f3c9d82bfec6b8ce2edf5aeaab4fba6d9b72e95c9638ae856af76fe598b088f6ddabaf8c1aa1af5137cbf681819f4e4db91ae336431e7c1c5363673ea9b332393003e031ae9bf4b20dd5a4a7859365ccedb10c2e2e226e1684f249afca9268c23352fb36e9ebd18cf254b9e6c70268275c5b83ff8b30c7abea185f4969bb3673330d959675f76b6bd839620c8dec153d4a7d79d012d994211acefeebf8b15f0f07fd7a4420e5328f7ec096d3292574d492698fb159987beb422ec5c620a211bdff153e57f567ac37843ca7dc3844961836f78ef5f590d9d1d8c29c9797060b2386dcb0b3d02b7ffe3b2e25cb45f4ba0de07b71a9864e7e0a2dd895bf642f49954ca2a7f95007a2ffb7dfa570c6e124fe007e06d4b66aa8ce81f550ec3c1cc4ce9186425b348d191232c76cb4018703b42495f28e113c80411566c85d61523f71aa08976c181005d30b306168d2c5c88ebdb951603f190dfd3fe48ede2858e02a77b90fcdbdcc34fa5098c9f3cf6142be856da830f43d8ab50b990c29e4a957241946974fb63f8da961b82b11ad4034e90528548659e5aaf3156dbbfbfa8d83c3e07981758442aa3be462c5983908a882ed8fb298e3c6bce51db1fa61c0faed67f7014d9f7fab463b088ec32aad30c680f463009f785e2f74003a90bcc05c4dedc376a11daca09891c2cee6242d8ace7743525c6b8988b41612fdbeea48098605b597b4cf3b736205688e3c1cfb2c5d8e9a70f805014159b9bc02a6baf2cf5daeec39bd1057343572c776f66be721e139ac74a30f6630e165d498640b4a3074689e7ee7bb04a8d0611b9323b4141195d94b26d4147ae734f3002c644c671f8c971e539323ce50fc44b6e045faafd3dac798d67cbb4268707aa9ea783eedf2300989fce3e2a519775bdfb01a25ba57ec7d608700fed67cc95b4cad48e9301edbb790e79de8acdac2a6cf2d3201251e6931b2ac655dcab2a41a761082a3b3d565b27455a4a278f70b58ae17cba43cbb7865fa9915a5129d9e20f7ffbb8a46e7861c85da4d780a5a0af6b7396c7a64ba30023ad9871671c13a682b600f9c64894524ce34dc8ce1509e8cc5ad34e899833aa8f67933d2f6521e9c425f77e8a978bc052aba8c8d23c30dfb1dc40ada63d5743705b8a6dc053e918d8b5e7f6840b2d9810900ed076c6cdcf7e143d8a6a759a9dde1f10b714fa57d2e87350a9d3c57f973a9db4576f68703b901938504a87267da534c2b6f6bb517c229002df0a23cc7dbebf1b81e8412c13786ce60cff177b940a76e5b499de0d8cf8457ae43ae9908f3ae0cc22b9778ec04404efbf93afce4c8f015894d3e4ae82d11f61d41ad02e0be7cf7c8639b8d280dc152fd75e9d0efcbdb9b90502d757f7a2d386f8bd001a95753bf81308b1dc88da58b1f221dd4e8e5217345b7cb0b6fe940874920af33481091a1e3ed6024afab00eb8cd3e15e142d9b5e1446a4554923ce97cb385867786e95304fd8c62dc0de08ba942d7d7493120b4b4a54b379594dc0558381305d6fba675582f6d7761513470ffeb5de56e9448922b452ec437704f08699c4831684239f07162b6debfe74f2dd61ac707cb98aec2ad82a5829000182e20381e802207b7490375005b4ab7a45bfe5fbad53683f38a72527393440345e75a6a7ee7d63d82a5828000181e203922020ef12b88fae9a11c8ef6e7e43fdc9bb2059e9a0fc0e291d68a3c49c9c86b3421f000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x30ed5a3", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf9b127b6341a6e269f846e39956e38fe62f80c4ba8af19158913490d0d87aca3", + "transactionPosition": 88 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b4022", + "to": "0xff000000000000000000000000000000002b404f", + "gas": "0x43a0269", + "value": "0x1a3ede932ea26d50", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000007878219e3e0590780ace586801c6af561f2983ca1ad7268d41a4eb125eb08064de6fd3f4d9b98b7ead52c60ac58919e54c354f2dbfc689aa3b9cf5dfad2c937f4ac2eb1bb4c2c370a795f86b6ebe359c6f8f830294dcde42d14ee689d057aa9db003483040ac387d810cf29f32e6a5d52cc11e158b8cf21e0094a4697627d342027f95edcb02865213b28f44787c89c2ac5571e0728de3eadb465cfc2072da9f60e124cfb93b7d246b609bb3ec8ef3388efd231bc257757745e9c2aa3faf541294f3043aedf93bb889127de8e1458fb10461df73403003017c924cf4300df4468afa4fddd67d0d0733b8c8c31db8897163041e77509498f218757d2773e3dae955a7aeeefd8a83443182de39ecbfa995fc03fe7fdc55c719c8057f9a4c8576a584d9fe4f6b1541d060d8350ef3203cc2367660197004902c7f3d2811adea9a7a498e73ce55e48ab54b41c29312c21576e904c45c2d9360331b2faddbed720ea9a1b9791669cc9d7f00bd1d5ba70deef07dfbdd438078a7bd91e137b4ef11f95f73ff0066abd34ccc6995aef7549db39eaaf2903c5b3f12e6f2a22c33e71515ecd3797f9f675660677ed657863235563b44c6b3ee9c27b584b90fc1752ee2418c4c0982fb820956825f874f4f23e958243064022f46523520570cc99803877aa7a987fd19bbb6d4e150a68864b7d49edd4c8ddefde9093e9b3f7b7e6a3ed19e7babddb4128f56de6cf96bd1e8904e2c915f3ac3beabcb7ec4995772c6eaccea909cffe8f7833dd6673427426ebd071743a95311d02867b2e1b82454dcd143978e42f968ccd112606dca3c83fa8365e5bcb170d4b7483fb0006ef832120a065c4fec044678551b78afe8a9a0c75af5d5c2b1c3aeaa02c47c63ca538d1e45ffacf6ea891295b264f2802e0b8031fc297d94f177edb50b207c20ef742fad5b9788cee3dabb6a6840f2bd91714e5cab90b173ff8c576544abdb8108f995c0dad05db88e13fc829b80341e2921d74b61fb063bf7e15df5e06f00d03a9f0c98979243ef53c5a1e78b1c592d901310b8efbe96b72ddb6c2e73e353cb3b769bcf83cc603bfd4a76643927b256c977ac06e3f679e38b455305df070ac3bb320390d3bc1fe6a6a0f5ad0a2c9a1fb8a1229d462b6289d5bfbbe613acf29d588b1888a9a3fa7ba9075d67dec51a97dc0e6a94ec455f15fe9173219a21a60ccae610697f44776dc63c07c8b0bc76fe20e17db74014ab5b3aed158c7f6db1cc0d339d6d72b5c2143574af4931306f800d754790d2703d5a7b15344de0f78a2e4aff46351654e4086b8807a6c93a74d5873a3c3d82a4572a0a9c8ba6f39f13effc3ae5451422e27b9ce09a0a5781f22d5b173352c76c969fa192c7ad6dcb8f8159e710fe8eacad3966df41f61375c5e74f3b5d8d8915faa5725a5301a4aca928cb9235aa184a7a853df2a384664e9dac20eb12d0b08c014c21e9b8611d516b5ec78093d8b9d10b8b27438958e2b89f4701726a3d9e3ec3d9a74418c0bb63dbde0e163fa2dc77560a4deaf90b691bb4fe9b11bfa00206245df864d52c75a73926395b7fbcf65f5ec4b87c8911d4d6ceb45b2a8313ed8e9136ab6ae614032e42cc3b097925cf6d8a6aef13a01a0ee363627ac36c8e0b82be6f304baf2598631bb0f24bd12a27147aa8b4476b9c29ba54f27339b9c3061d7729b78f26de21a701b8190c4d8adefaf926b8aa9212a849f34c50a8706764adb4ee8e3888f69e28b47ba3a320128254d7f7d994e0b9084e01fa3162ce7eed9109edb81dd36c8e96c226adb2c6660f2142c240213f721d134cb27afdcd1675fd19d517eaff5a3fc7693238f50fa55a2666bfdda36f1f8109e85471505f3c73b64bad8bdb801ee8ca53130d253c31573129359854ab14ae3eabe228ebea10bc1664ca4cf47b1888432b5766b9cb5467119ea7ad7ac2b20f0692b5c846dd9658babd9eb787b52d38d7feda3b3500f7850d8f1b6a94de3e79e5f6068ab487fc487790041a030747e086a375b473e33e477efe6d6b9a823f8ed7d541b038b41f6b43e9d6fd91a3ece1537843ea8a1a7266411f4b39c5573575b16a4293194f10a007f0335d984b4b06ff1b68181fcfa1b8f65d97358b248dd2c040de6fb99c3d1501620e7be07c32c30be700820f91312d6519824af63b2b171319b17813ab66487cccf75177cdb023122cf5c21040843a2c917cae89f34d870b4dfa2b867c49c96a06f073f20468226aa3e7b89454f79fca095ec962a28c5c40efd492d68ecdfc4da742368fd6956288b4d5144e7a826a805bbffdf1c46bb9b37069917e2fa997e375d5ccd9a6f646ba803eebf0bf1f1ffcf9d366e1862c5dc816d6db5aadbee419841a8cdde740f73afe869a91a6899c96f2993a1c882b55b37a1032639d1968a9e4046b7af3007d0293af5e4b74389847d6a7faa602f6f97610a14b2316c9045bfb9698c95a6da1bb682f44a269eea77e633cabb12b1d5993a97032b85a929584407182a0349ec834fbc6c87e5198e59ac0eb6e9bc83ad4ceca1bc7bca8cb34349e342e22ba2aea22125d46ebb473c47f6269b73d597fa9f82bbc70ea5198b7f48e016392d189125a15888054763581c87424a486b22e775c70ae9913d57df4428af047814233ef6998f4ea5179318af9872bde490053bd8f778257d599d0f481444f1c922010273acf2026fddc17dd6e5b24184515f6f7f10b5b300000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x80a80b", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc30e8b748b22427010e409aece99f62953c57de4f68383af4fd363beb0801e8d", + "transactionPosition": 89 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002b404f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3ea6944", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002b404f19e3e0811a045b29de5820d92f7e1060a1f96fa21101609789a7b51e4dad941c5ed5345bccc93f19e12cc45820e886746cf4ddfc8999519b72e3a9d45b7d53ebdc4865e810d9d7b78a186c479e590780ace586801c6af561f2983ca1ad7268d41a4eb125eb08064de6fd3f4d9b98b7ead52c60ac58919e54c354f2dbfc689aa3b9cf5dfad2c937f4ac2eb1bb4c2c370a795f86b6ebe359c6f8f830294dcde42d14ee689d057aa9db003483040ac387d810cf29f32e6a5d52cc11e158b8cf21e0094a4697627d342027f95edcb02865213b28f44787c89c2ac5571e0728de3eadb465cfc2072da9f60e124cfb93b7d246b609bb3ec8ef3388efd231bc257757745e9c2aa3faf541294f3043aedf93bb889127de8e1458fb10461df73403003017c924cf4300df4468afa4fddd67d0d0733b8c8c31db8897163041e77509498f218757d2773e3dae955a7aeeefd8a83443182de39ecbfa995fc03fe7fdc55c719c8057f9a4c8576a584d9fe4f6b1541d060d8350ef3203cc2367660197004902c7f3d2811adea9a7a498e73ce55e48ab54b41c29312c21576e904c45c2d9360331b2faddbed720ea9a1b9791669cc9d7f00bd1d5ba70deef07dfbdd438078a7bd91e137b4ef11f95f73ff0066abd34ccc6995aef7549db39eaaf2903c5b3f12e6f2a22c33e71515ecd3797f9f675660677ed657863235563b44c6b3ee9c27b584b90fc1752ee2418c4c0982fb820956825f874f4f23e958243064022f46523520570cc99803877aa7a987fd19bbb6d4e150a68864b7d49edd4c8ddefde9093e9b3f7b7e6a3ed19e7babddb4128f56de6cf96bd1e8904e2c915f3ac3beabcb7ec4995772c6eaccea909cffe8f7833dd6673427426ebd071743a95311d02867b2e1b82454dcd143978e42f968ccd112606dca3c83fa8365e5bcb170d4b7483fb0006ef832120a065c4fec044678551b78afe8a9a0c75af5d5c2b1c3aeaa02c47c63ca538d1e45ffacf6ea891295b264f2802e0b8031fc297d94f177edb50b207c20ef742fad5b9788cee3dabb6a6840f2bd91714e5cab90b173ff8c576544abdb8108f995c0dad05db88e13fc829b80341e2921d74b61fb063bf7e15df5e06f00d03a9f0c98979243ef53c5a1e78b1c592d901310b8efbe96b72ddb6c2e73e353cb3b769bcf83cc603bfd4a76643927b256c977ac06e3f679e38b455305df070ac3bb320390d3bc1fe6a6a0f5ad0a2c9a1fb8a1229d462b6289d5bfbbe613acf29d588b1888a9a3fa7ba9075d67dec51a97dc0e6a94ec455f15fe9173219a21a60ccae610697f44776dc63c07c8b0bc76fe20e17db74014ab5b3aed158c7f6db1cc0d339d6d72b5c2143574af4931306f800d754790d2703d5a7b15344de0f78a2e4aff46351654e4086b8807a6c93a74d5873a3c3d82a4572a0a9c8ba6f39f13effc3ae5451422e27b9ce09a0a5781f22d5b173352c76c969fa192c7ad6dcb8f8159e710fe8eacad3966df41f61375c5e74f3b5d8d8915faa5725a5301a4aca928cb9235aa184a7a853df2a384664e9dac20eb12d0b08c014c21e9b8611d516b5ec78093d8b9d10b8b27438958e2b89f4701726a3d9e3ec3d9a74418c0bb63dbde0e163fa2dc77560a4deaf90b691bb4fe9b11bfa00206245df864d52c75a73926395b7fbcf65f5ec4b87c8911d4d6ceb45b2a8313ed8e9136ab6ae614032e42cc3b097925cf6d8a6aef13a01a0ee363627ac36c8e0b82be6f304baf2598631bb0f24bd12a27147aa8b4476b9c29ba54f27339b9c3061d7729b78f26de21a701b8190c4d8adefaf926b8aa9212a849f34c50a8706764adb4ee8e3888f69e28b47ba3a320128254d7f7d994e0b9084e01fa3162ce7eed9109edb81dd36c8e96c226adb2c6660f2142c240213f721d134cb27afdcd1675fd19d517eaff5a3fc7693238f50fa55a2666bfdda36f1f8109e85471505f3c73b64bad8bdb801ee8ca53130d253c31573129359854ab14ae3eabe228ebea10bc1664ca4cf47b1888432b5766b9cb5467119ea7ad7ac2b20f0692b5c846dd9658babd9eb787b52d38d7feda3b3500f7850d8f1b6a94de3e79e5f6068ab487fc487790041a030747e086a375b473e33e477efe6d6b9a823f8ed7d541b038b41f6b43e9d6fd91a3ece1537843ea8a1a7266411f4b39c5573575b16a4293194f10a007f0335d984b4b06ff1b68181fcfa1b8f65d97358b248dd2c040de6fb99c3d1501620e7be07c32c30be700820f91312d6519824af63b2b171319b17813ab66487cccf75177cdb023122cf5c21040843a2c917cae89f34d870b4dfa2b867c49c96a06f073f20468226aa3e7b89454f79fca095ec962a28c5c40efd492d68ecdfc4da742368fd6956288b4d5144e7a826a805bbffdf1c46bb9b37069917e2fa997e375d5ccd9a6f646ba803eebf0bf1f1ffcf9d366e1862c5dc816d6db5aadbee419841a8cdde740f73afe869a91a6899c96f2993a1c882b55b37a1032639d1968a9e4046b7af3007d0293af5e4b74389847d6a7faa602f6f97610a14b2316c9045bfb9698c95a6da1bb682f44a269eea77e633cabb12b1d5993a97032b85a929584407182a0349ec834fbc6c87e5198e59ac0eb6e9bc83ad4ceca1bc7bca8cb34349e342e22ba2aea22125d46ebb473c47f6269b73d597fa9f82bbc70ea5198b7f48e016392d189125a15888054763581c87424a486b22e775c70ae9913d57df4428af047814233ef6998f4ea5179318af9872bde490053bd8f778257d599d0f481444f1c922010273acf2026fddc17dd6e5b24184515f6f7f10b5b3d82a5829000182e20381e80220602e2506c0e5e031448c0607714e86e9c234bfdc00bb8639580eec0e8ea41373d82a5828000181e203922020d5796125fc67380346b8b2734a81ff08e1e9f457606ef2a4528ea411888fef23000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x312ead6", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc30e8b748b22427010e409aece99f62953c57de4f68383af4fd363beb0801e8d", + "transactionPosition": 89 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc336", + "to": "0xff000000000000000000000000000000002cc33b", + "gas": "0x3528aa9", + "value": "0x8abb7a55e7bee0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708198b12d82a5829000182e20381e80220a4c966fc3621b677c749e58fce4a19d87f495a2bb82e5f5a294900321f38ba3c1a0037e0c2811a045b34061a004f9b81d82a5828000181e2039220200a7617f7ebeee2028f3677e7d760301d0c595d3d786d2daef70aae4cc7fb162100000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x24c0262", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdd94aee23e9cfa008afff1f60c7eb2a27e2896878f2be8ab834cd71caf5d3ff8", + "transactionPosition": 90 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc33b", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x33fcd9a", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdd94aee23e9cfa008afff1f60c7eb2a27e2896878f2be8ab834cd71caf5d3ff8", + "transactionPosition": 90 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc33b", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x32f0852", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdd94aee23e9cfa008afff1f60c7eb2a27e2896878f2be8ab834cd71caf5d3ff8", + "transactionPosition": 90 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002cc33b", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x31cf2e1", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f9b81811a045b34060000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x476eaa", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e2039220200a7617f7ebeee2028f3677e7d760301d0c595d3d786d2daef70aae4cc7fb1621000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdd94aee23e9cfa008afff1f60c7eb2a27e2896878f2be8ab834cd71caf5d3ff8", + "transactionPosition": 90 + }, + { + "type": "call", + "subtraces": 2, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000020bb1", + "to": "0xff000000000000000000000000000000000020d3", + "gas": "0x1d9e63c", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000408181870819321dd82a5829000182e20381e80220613a2f762e0bbf0a1fdfeb573b16c9ef105d5ce19c5ec21194cfccdfe2c9c6231a0037e043801a004f8a90f6" + }, + "result": { + "gasUsed": "0x1653202", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc4a33ab23d55c88a9eb2745a97e37dd6b44d96b9f14a00a9a1c06df904b759c3", + "transactionPosition": 91 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000000020d3", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x1cea250", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc4a33ab23d55c88a9eb2745a97e37dd6b44d96b9f14a00a9a1c06df904b759c3", + "transactionPosition": 91 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000000020d3", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x1bddd08", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc4a33ab23d55c88a9eb2745a97e37dd6b44d96b9f14a00a9a1c06df904b759c3", + "transactionPosition": 91 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bbcc3", + "to": "0xff000000000000000000000000000000001bba4e", + "gas": "0x608c127", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a002138aa5907808c23dc69fd600fac65df2b08b4ee5595049dce3df8a1ca6c1a0b47067300709c7bba1e1b7edf0f1ae8a11dced9fd646c87042bbbfbf8e2e77651c10597cb746fa0ea2caaddb219a8b79a0fdd9bc84242471f8b097d6e6ee1dc547c7f068905f103fff2cb2220608de1f525e034d46bed5968ad6799e44af0ba7805e86c2124dc262c3c6b8d73c10ab458a651ffde6b77a4621bd6af909eb88bb583559717d37c0363d75329d4f444c47a71e44670c1d7c9f6e08fcdbcc8fe70542e445893dd5ea51f205a5d4695eb2ff2a484df3be74a6462cccced5c7c552ec2e3568d02d4eab26e05da573b652f1c5a6bd51092350f83f803a8e496c069e866a9bf26c956bbeb82488d4410d2cab92a151abcc41526e74a57b4131ddcfa7c2d3e0706b6e8310481e5d7e2bf283ab1ae5650716b4ead437aed5bc50aa57138e976e885c28a1fc6e6e9a09619974b09bca55f34348d718ce52829d085704dab7715d49c2541c4a9301c7b45d85526e4829b4c07505c07508879e27a1f61bd261bf5d64d599f7ba195237ddae768b0fd06c377c24beffb203055e3ffed44a4a8584f5433ab586182a403f41d2ed434452ba32a08f9c24d8fe621b8bfc6c7e3d914c706e589e68563c1ee9c18f561d569dfc486f6765e30bdf165806e41b0648e82f1c9ebcdf574145c3c3bc6b92ff866337d46005ef56b786233c9d87f8793081bb7f903cb1e783152b482e86577a5df8cdb63744a47ab8f6d27b6380b9be7a4c53aba1f8b985e06e9623280468ea84aae5c6b4835a20fc43a40f4a0bbbbf2160586f5072fdbf999a08763587c500af0629bcd25bdf32c542e7e7af6835a6949527b804379ea137a17c623aa5917c802756919ba64a76ea8dcd49cd3478dd5d2f6d8d2f00204c2028c1b49136d750360613e277542de1930ee1559c581f1a6b3842abf100034380ecadd6dd1bc13adef306133549d463f45dc29968ad5c15b0adf9c13e051ebae7cf2a5bcb1a858df6daa3e19c2f01120af1b38e1ce0412135c8a1426fc0f009888ecdc65e0d91ad80e1fd42856598cd06e4dfbc6a9c7600fa56a48f755599fac92e816b9dce6982768819a6e24ecd3e0d8ab12fb35283b974796b9e304cdd961b15770331f0186c6db6966ddbbf7bef2a74ea8881ddc1b5cf22c6f764b0e68782b6df21f4dcd69ead4744298eb49c1ad87f086c1236e1e3ca477a7048c480c781254bc5a68886f5d94ff26d00439ac1805266f20547f11753b0d50e724e8930ce5be8af4d3b325d40eaca084d7050143831ec1bc40b5ad1b06aad472a0ff5cefdaa94aa33e5ad0ed5299436799a9a6b42a1222f56995de79fd7979b712eb8212810f8651832c7f4341ef4be3e2801de16898be3fdcd20f45aecc718c7066e160e0867afb2ac813a572645b20857d24388edfb15259db541187c2c5cb2c438b2068c64b2bf275004dabd8259134108df796d4a9603f9e26626521c718a28fc9a10a30613abb792b2a9e9d464295867fdd94968134d3895e7efc6ce4c884d213a81692625c28db3e18d5e03e3e6dd39fe9894eb02c50689c09890cdd1a27ce56623acd2381e7b38250bfa880d59049cdf1adecfe224446513fca3df71db5b5500ca038fb9569adf1f526926b9bfa66e27df4470baa2f1429bb1c2676cf8877f173c8d8c2e0c152e17cb6794cb80083cce0a2e7a57c31a5ba41913ed9806f2d50e3a889fdc888e24841b62b5f1c9cdc74f1152e882fb789846533a5e4caf0d5f1ea050856b5f7f82f8728a859107978cd95d62652a1e7191b1a296096dd8cf250de8b22e948d4102f2955b61a3effa2b667a49cb58c688bf2510a018e360db50394e57c41d39b866f0144e9bf9856108c83572149b6621a5f9491d732ede5b7bcd7b34f810066df96dbcc341b32a0c7756f05345b66ffddffc5eaa5de22928efcc6c80a949374db5a546008c47ef4fbf296b316dd03a57a40a89c730b9f3602df445547ea5ba4262e51919e2cb4c71ed4f5c63740d347c1471d05a9a266fe318ec306fdb9ddc6211caef273867e3e409dc317e4e04d49e40b474e29456f2f3e6252d2a18ed90e5188d13c74cde5e904d8cdb95c55c636c83c085c58ebaccea0ce2b1ec5b0696932007b538bd977ca26f630e9df8a16c2b031692a160ffa45f5d6e9a625412163c2771a54c4432d61f49ce9de85b475dc33eae4b2886cf4343ce2ed591a9f3f48b1b6bfafe3e59515ff9ee1895596e80c9295004d71ec645a4b840a37057c8b07b7ec4b2a2fa07c3624a48b34a49173b6713e85418fd52bf222ab43029c6d1a629633d78153423134ab11ad45b15376c4bf132fb440b81053df7b1b4f25761ea895ea89b5c512f753859fc182cef73d8a33b958d2cc2d4383f0ada70d8d6d466654f62130959ac09a4c380f8f552d8b1afb6ce83abd2fff317cb0d7b2b347a2b0b46e29ebb6a23810ac8e0c4b90dbd81e268f2ce8659fdf1da7c97c41096d9a0a19a69ccf3f7880d49b669384562670c4255e194bd26f3cca9773feaba4c280ba2e357a6c2981f5b28f711f730bb01db7401c184bda05241d71cf8c10694ccd1f852f9c849133bf5cd34dc343735aeabfdea12a318ebf2a94fedd7161d34c30d724f968741c1738ad0fd94e9667b291342901883f0ed894402afa5d3a113a640e230d4ed85b8fc2af361d15402b618eff62b5acc012fe9dc52b245b0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7ab5df", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x42d6ff73ca53378a960a6b23df67cf45e01045369c0dec7d21a7a9eabb2cc8f9", + "transactionPosition": 92 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bba4e", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x5beec87", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000082e8808821a001bba4e1a002138aa805820f39f2cdf9f09e7637320a65dc2e350ad731fba586b3fe5cc1838cfd58ff596fe582081773ad568e0121c5b3b01461b0b703611f4bf3317c88a87897e2e8e123489d85907808c23dc69fd600fac65df2b08b4ee5595049dce3df8a1ca6c1a0b47067300709c7bba1e1b7edf0f1ae8a11dced9fd646c87042bbbfbf8e2e77651c10597cb746fa0ea2caaddb219a8b79a0fdd9bc84242471f8b097d6e6ee1dc547c7f068905f103fff2cb2220608de1f525e034d46bed5968ad6799e44af0ba7805e86c2124dc262c3c6b8d73c10ab458a651ffde6b77a4621bd6af909eb88bb583559717d37c0363d75329d4f444c47a71e44670c1d7c9f6e08fcdbcc8fe70542e445893dd5ea51f205a5d4695eb2ff2a484df3be74a6462cccced5c7c552ec2e3568d02d4eab26e05da573b652f1c5a6bd51092350f83f803a8e496c069e866a9bf26c956bbeb82488d4410d2cab92a151abcc41526e74a57b4131ddcfa7c2d3e0706b6e8310481e5d7e2bf283ab1ae5650716b4ead437aed5bc50aa57138e976e885c28a1fc6e6e9a09619974b09bca55f34348d718ce52829d085704dab7715d49c2541c4a9301c7b45d85526e4829b4c07505c07508879e27a1f61bd261bf5d64d599f7ba195237ddae768b0fd06c377c24beffb203055e3ffed44a4a8584f5433ab586182a403f41d2ed434452ba32a08f9c24d8fe621b8bfc6c7e3d914c706e589e68563c1ee9c18f561d569dfc486f6765e30bdf165806e41b0648e82f1c9ebcdf574145c3c3bc6b92ff866337d46005ef56b786233c9d87f8793081bb7f903cb1e783152b482e86577a5df8cdb63744a47ab8f6d27b6380b9be7a4c53aba1f8b985e06e9623280468ea84aae5c6b4835a20fc43a40f4a0bbbbf2160586f5072fdbf999a08763587c500af0629bcd25bdf32c542e7e7af6835a6949527b804379ea137a17c623aa5917c802756919ba64a76ea8dcd49cd3478dd5d2f6d8d2f00204c2028c1b49136d750360613e277542de1930ee1559c581f1a6b3842abf100034380ecadd6dd1bc13adef306133549d463f45dc29968ad5c15b0adf9c13e051ebae7cf2a5bcb1a858df6daa3e19c2f01120af1b38e1ce0412135c8a1426fc0f009888ecdc65e0d91ad80e1fd42856598cd06e4dfbc6a9c7600fa56a48f755599fac92e816b9dce6982768819a6e24ecd3e0d8ab12fb35283b974796b9e304cdd961b15770331f0186c6db6966ddbbf7bef2a74ea8881ddc1b5cf22c6f764b0e68782b6df21f4dcd69ead4744298eb49c1ad87f086c1236e1e3ca477a7048c480c781254bc5a68886f5d94ff26d00439ac1805266f20547f11753b0d50e724e8930ce5be8af4d3b325d40eaca084d7050143831ec1bc40b5ad1b06aad472a0ff5cefdaa94aa33e5ad0ed5299436799a9a6b42a1222f56995de79fd7979b712eb8212810f8651832c7f4341ef4be3e2801de16898be3fdcd20f45aecc718c7066e160e0867afb2ac813a572645b20857d24388edfb15259db541187c2c5cb2c438b2068c64b2bf275004dabd8259134108df796d4a9603f9e26626521c718a28fc9a10a30613abb792b2a9e9d464295867fdd94968134d3895e7efc6ce4c884d213a81692625c28db3e18d5e03e3e6dd39fe9894eb02c50689c09890cdd1a27ce56623acd2381e7b38250bfa880d59049cdf1adecfe224446513fca3df71db5b5500ca038fb9569adf1f526926b9bfa66e27df4470baa2f1429bb1c2676cf8877f173c8d8c2e0c152e17cb6794cb80083cce0a2e7a57c31a5ba41913ed9806f2d50e3a889fdc888e24841b62b5f1c9cdc74f1152e882fb789846533a5e4caf0d5f1ea050856b5f7f82f8728a859107978cd95d62652a1e7191b1a296096dd8cf250de8b22e948d4102f2955b61a3effa2b667a49cb58c688bf2510a018e360db50394e57c41d39b866f0144e9bf9856108c83572149b6621a5f9491d732ede5b7bcd7b34f810066df96dbcc341b32a0c7756f05345b66ffddffc5eaa5de22928efcc6c80a949374db5a546008c47ef4fbf296b316dd03a57a40a89c730b9f3602df445547ea5ba4262e51919e2cb4c71ed4f5c63740d347c1471d05a9a266fe318ec306fdb9ddc6211caef273867e3e409dc317e4e04d49e40b474e29456f2f3e6252d2a18ed90e5188d13c74cde5e904d8cdb95c55c636c83c085c58ebaccea0ce2b1ec5b0696932007b538bd977ca26f630e9df8a16c2b031692a160ffa45f5d6e9a625412163c2771a54c4432d61f49ce9de85b475dc33eae4b2886cf4343ce2ed591a9f3f48b1b6bfafe3e59515ff9ee1895596e80c9295004d71ec645a4b840a37057c8b07b7ec4b2a2fa07c3624a48b34a49173b6713e85418fd52bf222ab43029c6d1a629633d78153423134ab11ad45b15376c4bf132fb440b81053df7b1b4f25761ea895ea89b5c512f753859fc182cef73d8a33b958d2cc2d4383f0ada70d8d6d466654f62130959ac09a4c380f8f552d8b1afb6ce83abd2fff317cb0d7b2b347a2b0b46e29ebb6a23810ac8e0c4b90dbd81e268f2ce8659fdf1da7c97c41096d9a0a19a69ccf3f7880d49b669384562670c4255e194bd26f3cca9773feaba4c280ba2e357a6c2981f5b28f711f730bb01db7401c184bda05241d71cf8c10694ccd1f852f9c849133bf5cd34dc343735aeabfdea12a318ebf2a94fedd7161d34c30d724f968741c1738ad0fd94e9667b291342901883f0ed894402afa5d3a113a640e230d4ed85b8fc2af361d15402b618eff62b5acc012fe9dc52b245bd82a5829000182e20381e80220c76afb4b41e42e410d2e7e6626ff6b8b33164323e57d3ce64128fbc5010c6860d82a5828000181e203922020077e5fde35c50a9303a55009e3498a4ebedff39c42b710b730d8ec7ac7afa63e000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x316261a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x42d6ff73ca53378a960a6b23df67cf45e01045369c0dec7d21a7a9eabb2cc8f9", + "transactionPosition": 92 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bbcc3", + "to": "0xff000000000000000000000000000000001bba4e", + "gas": "0x6adeeba", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a00213857590780b1007b4492daf38cf92b272c7955ed27cd37a1eb17c5e88286ec68b3e21e1461cc1e34069af515a13e1835681f32b368b80a88a4bcb813e6e266251c6f33426c0ee3e8522218409424fdfbb592efc4d5ba8707eb6179df411f1524ee1949b98610e80b9d4b7be48c8bab6b64122a65146aa69f06a7bfe7dcf37596e1d1ddd1b6bca0d1aee19bbbc64f93a79e325cf2e69313642489c202aa3f20834782f9dc0b39e47d670d7ee6a9582d42bd06d77442076d3af3fdefacf2883c2bf52a87e25395748a4c99ff87d6ed45d6343c695bd515adaabeb7a79f4a333ff05c0e73c138a2dc3e540261bc57d7517327c8f81d0a8ac554f0b4407913e9dfffd128c3d0f8a7231f6ecf1023174389f7857f93ab45187fe90ed964e1b63cecc666b202f3f8031eb67cdd680f5ac27bb20aafe403baef23887c9e5983f5222f29bebb67b81ed4e9e4bb32b5c93120b2a13cd83b658d8f5989ff15d3aa81f86fca8850e414c2ea642b71d30bdd6a394394abc13e0d29b32bcbf9cb1843c07124c1c3a960b16c9796b59d275b98430fba4ffcbeb084f96238b1e936727613135ae0dd145fb72a87494f04f54f4855b2e4e699b1eababeabdfb405110110e0e55a3f93c7e89fe6c41f12f79f1f49c45331d2cc3e4653902901092b2b8ef047e2c717aa633d2596184a4b41834bc8473c80348699db9f228964bd4c86f6a8dd41963b3f3cf6673dc47a7fad3b13cda21489782d85aec609a2b03fe1ee546c367eaeba5e010456c7bca14242391787781874d71b7c04e302c7eea0731e869cf55f43a50b4660ef90b59b498b3c00eac63b5f163f921ee30438a214dfb3f1a152578b59d12ffac6f41613edfcacf34d517ab03fee2559f97c97cf212bb8aaf891ca30703ac9d1c06e1ddda63152027199c6c9e6f84e57b801357d3b9db691330d7ad1832cd70a9e821847e10113500d47424c7b7964aee8003dfe2f00f96d31575ac5228e7ae8dd60f1987fa5e2df0b9f733db586b65ad998ad6d88d622756de188a9b03c2311163a6e4771566976fbe573104a637f4888947cf15c0c0655dbce862b386a3ca53081b3b76f81958a5bd20bb5dbcdf009c2a7088c3d8784d51f682669b8f1b45f4764cd073aaa77a8089cee546ecdae57bd94b36d24bfb7e9199407c3cfcdf47fb4b6f681a8dd46017b1da29f0ec3b1d18f14166c5089c7ef26620f7503ddf6d4c1640f4d12e820c8ccc72598d33702f7d2483712306c217b646f938510c81afcda60bfcfbad6a134e18654b85180e4b08285b6b2c79c561af610d6c8ada60e88bc28e7ef39844611db8cb095a2ecfa622b31397cb93711e9207b77cf53867bb9993a97822c38fb055750c9a8022a0f0d90face484c80044eb15c2cafd7cdefb48476ab92cd458e5cfddd975ae85439522466afe858a792d929451c9fc080817e1be3056c3380f912b660d3ec2bf0d42a6fbe609d410e25367b219926c4105110766c1286e389ee46e47d3b1d3434b5365d851bad4e1f77d3e45f8ee3f65365c50f88ad65fa31b0b5701f0d7e3336d462d271a518c1642dbd94f8282faf96f00d85ef614ed27d10bd8479de6ead9109e94091154c162276d5c66fe99b3ffe43e5cb2c8dbc545a0dd131c4a30dc7a0eddb3ae96cace625bf72b9140ed306d90ac5057fbfd4b4b9679e1392f8df971cc1cba61a82779c41a3ba13ca860541c5aded732e46aa71bb64385f83ffa339548afbff8bfa9d289f41125e47fade43eaf9ca210a0d850b1b313023290bf0766f252b83450325221f80262b144db67c40f49b410cad37925cab42e97b1b9f37a485df0ff0b37ef87632ae07c593730bed2538a47fa2717561e0cfaa1c7b3d81e82c0947aac9ea8ec8a40055c1bbebd8cd5e8352bb82c4bfc5c90bbcbb6e6b5388fc7f158b7d1c6d4a4eafb0e496e801ac010aa6989b4d6bb0490642aa76bb4349bee326a8b00c06e9ba01ce15165a4dbb5e7da927fa59cd9175f5ba95e800ff43cdb1e65e19d48e552b2608850ef47e9b40ca7b1c0a3464937f4480211d6f4baae37ca8c956dfcebc6fe4e9389ac61c359ed6a4c9fa2edcfc70ded57ec626f4b7fd758613b104a9caf590cd07c4dd82140b06cbda7aa6d051d7b7d3842b4bd19971ae089e0a6816b3fa483a3d43fe8a4c722ab531a380ba1f50214f63cd0478ba8a1692e724d0c8f66cf4ef24f105e19d6dd2f4a889d5870009daf12259400e055f47799a8af59e5af090f063000bf9249e0873416878ae2e5d96b99559ba442fc66135822748dca90a17b62aba124c9dce11f4bc10de045ef27c250b0bbd300340abb044c0650646aed8570e3001c881254040027e8e8292f539b3cc41cade3427ce1e2fac0925efae5202961ed0eda80f590a19af89bef00fddabc96ee60dd5b9a7354bfd368de599d3f39fe6d00ca22ca43411abdd9519c4ba55e35799b1769f23509637131f493bd9561bb6c284a0711f22b702d7e3d8bc1adcc135bfcf7612277bf88801919448799005e25fa86afabd45820ec3178b0d62de904693b93366a31c9963bafb678f81168abed95b5efdfdb88f181bed43e298bc88ebce505591e9a31f1de187e713f22c1b01299f73e1e69c396d246576157bbec8200ac414ec7ea11c880682b2c2727d4ff5c28b1a049e3b369d992b932605b571f87d8cc9bae645ee150d9624f911e704c64cf9a3c28223d60000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x7aae4a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf099c22c6ed9edfc55c1546600f7467d56f5e43350d300b9e11e76de226edc2e", + "transactionPosition": 93 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bba4e", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x66421af", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000082e8808821a001bba4e1a002138578058206b302393a7aff44a8f259b4ca67a22a687cc8e7df35eda9dc30dd32aed8a1afc582091901695a71c68c04728449c97e6f2842d92b0bb83026300a51b3bc8583385ca590780b1007b4492daf38cf92b272c7955ed27cd37a1eb17c5e88286ec68b3e21e1461cc1e34069af515a13e1835681f32b368b80a88a4bcb813e6e266251c6f33426c0ee3e8522218409424fdfbb592efc4d5ba8707eb6179df411f1524ee1949b98610e80b9d4b7be48c8bab6b64122a65146aa69f06a7bfe7dcf37596e1d1ddd1b6bca0d1aee19bbbc64f93a79e325cf2e69313642489c202aa3f20834782f9dc0b39e47d670d7ee6a9582d42bd06d77442076d3af3fdefacf2883c2bf52a87e25395748a4c99ff87d6ed45d6343c695bd515adaabeb7a79f4a333ff05c0e73c138a2dc3e540261bc57d7517327c8f81d0a8ac554f0b4407913e9dfffd128c3d0f8a7231f6ecf1023174389f7857f93ab45187fe90ed964e1b63cecc666b202f3f8031eb67cdd680f5ac27bb20aafe403baef23887c9e5983f5222f29bebb67b81ed4e9e4bb32b5c93120b2a13cd83b658d8f5989ff15d3aa81f86fca8850e414c2ea642b71d30bdd6a394394abc13e0d29b32bcbf9cb1843c07124c1c3a960b16c9796b59d275b98430fba4ffcbeb084f96238b1e936727613135ae0dd145fb72a87494f04f54f4855b2e4e699b1eababeabdfb405110110e0e55a3f93c7e89fe6c41f12f79f1f49c45331d2cc3e4653902901092b2b8ef047e2c717aa633d2596184a4b41834bc8473c80348699db9f228964bd4c86f6a8dd41963b3f3cf6673dc47a7fad3b13cda21489782d85aec609a2b03fe1ee546c367eaeba5e010456c7bca14242391787781874d71b7c04e302c7eea0731e869cf55f43a50b4660ef90b59b498b3c00eac63b5f163f921ee30438a214dfb3f1a152578b59d12ffac6f41613edfcacf34d517ab03fee2559f97c97cf212bb8aaf891ca30703ac9d1c06e1ddda63152027199c6c9e6f84e57b801357d3b9db691330d7ad1832cd70a9e821847e10113500d47424c7b7964aee8003dfe2f00f96d31575ac5228e7ae8dd60f1987fa5e2df0b9f733db586b65ad998ad6d88d622756de188a9b03c2311163a6e4771566976fbe573104a637f4888947cf15c0c0655dbce862b386a3ca53081b3b76f81958a5bd20bb5dbcdf009c2a7088c3d8784d51f682669b8f1b45f4764cd073aaa77a8089cee546ecdae57bd94b36d24bfb7e9199407c3cfcdf47fb4b6f681a8dd46017b1da29f0ec3b1d18f14166c5089c7ef26620f7503ddf6d4c1640f4d12e820c8ccc72598d33702f7d2483712306c217b646f938510c81afcda60bfcfbad6a134e18654b85180e4b08285b6b2c79c561af610d6c8ada60e88bc28e7ef39844611db8cb095a2ecfa622b31397cb93711e9207b77cf53867bb9993a97822c38fb055750c9a8022a0f0d90face484c80044eb15c2cafd7cdefb48476ab92cd458e5cfddd975ae85439522466afe858a792d929451c9fc080817e1be3056c3380f912b660d3ec2bf0d42a6fbe609d410e25367b219926c4105110766c1286e389ee46e47d3b1d3434b5365d851bad4e1f77d3e45f8ee3f65365c50f88ad65fa31b0b5701f0d7e3336d462d271a518c1642dbd94f8282faf96f00d85ef614ed27d10bd8479de6ead9109e94091154c162276d5c66fe99b3ffe43e5cb2c8dbc545a0dd131c4a30dc7a0eddb3ae96cace625bf72b9140ed306d90ac5057fbfd4b4b9679e1392f8df971cc1cba61a82779c41a3ba13ca860541c5aded732e46aa71bb64385f83ffa339548afbff8bfa9d289f41125e47fade43eaf9ca210a0d850b1b313023290bf0766f252b83450325221f80262b144db67c40f49b410cad37925cab42e97b1b9f37a485df0ff0b37ef87632ae07c593730bed2538a47fa2717561e0cfaa1c7b3d81e82c0947aac9ea8ec8a40055c1bbebd8cd5e8352bb82c4bfc5c90bbcbb6e6b5388fc7f158b7d1c6d4a4eafb0e496e801ac010aa6989b4d6bb0490642aa76bb4349bee326a8b00c06e9ba01ce15165a4dbb5e7da927fa59cd9175f5ba95e800ff43cdb1e65e19d48e552b2608850ef47e9b40ca7b1c0a3464937f4480211d6f4baae37ca8c956dfcebc6fe4e9389ac61c359ed6a4c9fa2edcfc70ded57ec626f4b7fd758613b104a9caf590cd07c4dd82140b06cbda7aa6d051d7b7d3842b4bd19971ae089e0a6816b3fa483a3d43fe8a4c722ab531a380ba1f50214f63cd0478ba8a1692e724d0c8f66cf4ef24f105e19d6dd2f4a889d5870009daf12259400e055f47799a8af59e5af090f063000bf9249e0873416878ae2e5d96b99559ba442fc66135822748dca90a17b62aba124c9dce11f4bc10de045ef27c250b0bbd300340abb044c0650646aed8570e3001c881254040027e8e8292f539b3cc41cade3427ce1e2fac0925efae5202961ed0eda80f590a19af89bef00fddabc96ee60dd5b9a7354bfd368de599d3f39fe6d00ca22ca43411abdd9519c4ba55e35799b1769f23509637131f493bd9561bb6c284a0711f22b702d7e3d8bc1adcc135bfcf7612277bf88801919448799005e25fa86afabd45820ec3178b0d62de904693b93366a31c9963bafb678f81168abed95b5efdfdb88f181bed43e298bc88ebce505591e9a31f1de187e713f22c1b01299f73e1e69c396d246576157bbec8200ac414ec7ea11c880682b2c2727d4ff5c28b1a049e3b369d992b932605b571f87d8cc9bae645ee150d9624f911e704c64cf9a3c28223d6d82a5829000182e20381e80220b004dbb253f292969d2a45ab68bd917a5bb6b2f9031f74638403a29361b03e0fd82a5828000181e203922020077e5fde35c50a9303a55009e3498a4ebedff39c42b710b730d8ec7ac7afa63e000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x38bc17e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf099c22c6ed9edfc55c1546600f7467d56f5e43350d300b9e11e76de226edc2e", + "transactionPosition": 93 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bbcc3", + "to": "0xff000000000000000000000000000000001bba4e", + "gas": "0x65623bf", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a002138a5590780938809e4804b3536e70cb0531840c36f70f8131906dc55738323fd254b0f8f18832e0f414eecb7ab09e0c9fa971b9524a2df8199e2302002c584d0cfcd96dc2551dd37f06539dbd8721dfda01c3cdb7e8dbb9d4a3fc353056055197b0841ea550a104147ed57d86249ee9838f05fc23ac3656bd18c527862d624b75f4fb29cf1c86cdb9b232b44e35695742b71800b68824d4d88eb47c7ba20babaca7beb5fb74007d26e5f285fce5ecaeac4bad411b8987d887dc99d0030b2861eb7dc4700a9b82b3371e8454fab416288f44520e1308947cc468c68b489340804774cf5c8a4577f7749a84202b8f855170c681dcd0994bbdd6753f174ea6efbadf483278d57cbc93eb3dc9dc841cd3e7f70c93280117a3c3080a498a56b9dc66fcad5732ec70cec935046665103a1d0ed7629409d8cb0f5b8280ec38b07752a7b52bad8142d8c1a1bdf9fa05da66508e62ed1076ea78f4af3062b5546c4fe03dfcf49079c442d4ec450dd90a193ef337323b15c6076feb095dafda9640abcbe8a7c4233226c991ad259bcfaf2db6b55117614b6dec5dcbaadff4fb2ecc0152f74b6f78d7eea32ba4262e26e52246a91c2862f1477a38ea6bc1fa5805a16996af8b23fdb702f4ee31616c827af71b730c19392162b08c449cb72407d720b27ffd9b2b59fc3370db1eb60969f3b4f7c52796d8047963c2872bda2dc3f53ceb0e5bd9861e9ba55fe2ec7efd872b47f4841f4629dc0632197dff41ffdbb9f76176d9255d08a9f757c534b4dd6ecfb6f2700b11208d9f247d2b13661e6c761283d89824688d66049a1fbcc4dc6f623fe513f878a5c5a9de8b89482f9fe61e7dc9ba1f94d53b2162050f03a9056dcbdf255447e2a342e64a281daff99c80fc4460d23571a7bde6f3ec7c74e80adbf0e53b17e88b505d4bd5e54d4daa881819cdd5143d174130ace2e0a4ae39d81d37b2f2130b9fac40b14540bbaae81a0f156e76bb6c40000fae97789f1bd64a43f7ac4869b7d82b1dc569faaf5f4e578001dc2dc4e4b693f78464257d056de197ca2ebf24d02db47ed078a005a3653d3fecc334b6db1c37594781fac82018e230badade7730f75d55c29a594a73539a675fac512fdced85a95b79a874a585ff8a560c0e95bff0cfbd0790a96981373361d59342de7aa41b2592a98e6188626495895fc92b48fef4a34b39b07186fa9c1385e1d4cb12c1046f1483d1736f3881f98162cb507827c265c2f7d20f0a56334e75e056cc245fd34eaba22535ac6f6b81a94d78499a2659908a4af8ef74e19c6c1750589b1168a14cd43f4ba90eeabd5db32ca22056357aa79c914c87430f8e033e9d1ca73a60dd593c8d9b3150771833b871514f590d261ee8172649cbcb1c0af79cddf194261e0b0953f14edf1ded13a435e02dde8a7afa0aa2e858165fd467dacbfd8ad5cb59e51c11dadbab16a24aa34e8bb20a5d52bb4126dd9ef86420ec17a96cdf05aad211c7cec103a46c2b4ce956c73b51cb8421deef8ee19b13d8aa58daf0ae56e202436c193919b844b06fb9b21e9e2a900387a76e68cc654e16def4047035f485fc8b1618e987319d7950ccb42d6a70c8725e49dafff9a476a374a14cd09adef5a5c4b04dfa4887c6b64c4355d3a444c435f22275f7b5d8fc491236e62a0edeb5d2ac2a6d9d68ba7c3e1e6906a03d995934fbd9849b791ae648cdc0f1494ce758bff37b0116f3526cdafab18120f491323065fa345c2066e99d9ba2337bfec891a27e4ab0918d78652c329926d4186bccba63aba373b410e48301bebb03aadc8c36efed11575dd2ef1cbc5b9495dc498733dd4c7eb97c3019ec2f7a13da5c61f4130f4e89fad38b65b3bfaec1b846f70456b78869e25db44e67fbb97c6a680a9812b105c60b77074dca69c0d16a904da5388c5f935835f48e40eaf1e2da59fa2f5598b9653adf96dcaf14c68d5aadfa744fbfcb8688f195d6b8260b9f25015a58779b0ce0c00b1ef06f241effb557985c0680d131f9ba8e8f5d1bf351df45766fe4dacb08602352ac292f27e875c1adce61cc9a71bef08f567bb77e5f7586354711eb1c968bd4aa18966cbfd945b706e1cac579278aa5392da1204b1857c72b2ae68cbedf01c632e4e0aac3f88f691711d80ac557ae31ef84e69007fe74ff3195a3137f363a783418ab3cf1dc2df032e9d0ff227426b003249254b786dfa00b3a950dfd7225b6a372252e0ab276734ca6d61b86b268ca42a5aaf8134f9597ad3f6e6892d3d6aae36305cfc23c52cf519f882d61f8d43303fef819be2a11166fe83170cd28701662da9f08853b2853c09999197098cd894b0ac636192dee9d773a1d4299d93a64fe7f3b4c3897ba23652bc6635e6bbb66120e108e4d1f65ad78aa5ab3f170d7ca0e42626c279e685b3dd432f15a521c0a0e98e5fbcd0ba0d9eefcaa7b01d68b04b31c71ce9799a86f88410e84a3b61201ed738f737fd408cc284dfc7c66fef73490d4b39a0e23430833764084f60c98923296607f8c4fe4ca962118eac687c891fa444c9ccc72147f948be34b412cffcf5a8d635640388bea532cbca5cf32704c3139459d07dc2a82f77d2046008d1bd797a318b07a671879392e67e0bace396957faf2ed64393ea7ae07df1d70381b0bc3e2c70cac7c5f2274c87fac7c56bd4cd3af4297c538ec1fc4f8e862edbf759b59f052bf3aa6e87acbc2ccb78944d0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x6e6346", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x293e1eb922ef1b490b4d2c4c14b8e51c383ab38933509fdee3de8a7b4b96d0dd", + "transactionPosition": 94 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bba4e", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x618a1b7", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000082e8808821a001bba4e1a002138a5805820fe51bab9960b3c80d4c6c8357c418a5593f2c35c2a7337a94a6e6ca9486bb990582081773ad568e0121c5b3b01461b0b703611f4bf3317c88a87897e2e8e123489d8590780938809e4804b3536e70cb0531840c36f70f8131906dc55738323fd254b0f8f18832e0f414eecb7ab09e0c9fa971b9524a2df8199e2302002c584d0cfcd96dc2551dd37f06539dbd8721dfda01c3cdb7e8dbb9d4a3fc353056055197b0841ea550a104147ed57d86249ee9838f05fc23ac3656bd18c527862d624b75f4fb29cf1c86cdb9b232b44e35695742b71800b68824d4d88eb47c7ba20babaca7beb5fb74007d26e5f285fce5ecaeac4bad411b8987d887dc99d0030b2861eb7dc4700a9b82b3371e8454fab416288f44520e1308947cc468c68b489340804774cf5c8a4577f7749a84202b8f855170c681dcd0994bbdd6753f174ea6efbadf483278d57cbc93eb3dc9dc841cd3e7f70c93280117a3c3080a498a56b9dc66fcad5732ec70cec935046665103a1d0ed7629409d8cb0f5b8280ec38b07752a7b52bad8142d8c1a1bdf9fa05da66508e62ed1076ea78f4af3062b5546c4fe03dfcf49079c442d4ec450dd90a193ef337323b15c6076feb095dafda9640abcbe8a7c4233226c991ad259bcfaf2db6b55117614b6dec5dcbaadff4fb2ecc0152f74b6f78d7eea32ba4262e26e52246a91c2862f1477a38ea6bc1fa5805a16996af8b23fdb702f4ee31616c827af71b730c19392162b08c449cb72407d720b27ffd9b2b59fc3370db1eb60969f3b4f7c52796d8047963c2872bda2dc3f53ceb0e5bd9861e9ba55fe2ec7efd872b47f4841f4629dc0632197dff41ffdbb9f76176d9255d08a9f757c534b4dd6ecfb6f2700b11208d9f247d2b13661e6c761283d89824688d66049a1fbcc4dc6f623fe513f878a5c5a9de8b89482f9fe61e7dc9ba1f94d53b2162050f03a9056dcbdf255447e2a342e64a281daff99c80fc4460d23571a7bde6f3ec7c74e80adbf0e53b17e88b505d4bd5e54d4daa881819cdd5143d174130ace2e0a4ae39d81d37b2f2130b9fac40b14540bbaae81a0f156e76bb6c40000fae97789f1bd64a43f7ac4869b7d82b1dc569faaf5f4e578001dc2dc4e4b693f78464257d056de197ca2ebf24d02db47ed078a005a3653d3fecc334b6db1c37594781fac82018e230badade7730f75d55c29a594a73539a675fac512fdced85a95b79a874a585ff8a560c0e95bff0cfbd0790a96981373361d59342de7aa41b2592a98e6188626495895fc92b48fef4a34b39b07186fa9c1385e1d4cb12c1046f1483d1736f3881f98162cb507827c265c2f7d20f0a56334e75e056cc245fd34eaba22535ac6f6b81a94d78499a2659908a4af8ef74e19c6c1750589b1168a14cd43f4ba90eeabd5db32ca22056357aa79c914c87430f8e033e9d1ca73a60dd593c8d9b3150771833b871514f590d261ee8172649cbcb1c0af79cddf194261e0b0953f14edf1ded13a435e02dde8a7afa0aa2e858165fd467dacbfd8ad5cb59e51c11dadbab16a24aa34e8bb20a5d52bb4126dd9ef86420ec17a96cdf05aad211c7cec103a46c2b4ce956c73b51cb8421deef8ee19b13d8aa58daf0ae56e202436c193919b844b06fb9b21e9e2a900387a76e68cc654e16def4047035f485fc8b1618e987319d7950ccb42d6a70c8725e49dafff9a476a374a14cd09adef5a5c4b04dfa4887c6b64c4355d3a444c435f22275f7b5d8fc491236e62a0edeb5d2ac2a6d9d68ba7c3e1e6906a03d995934fbd9849b791ae648cdc0f1494ce758bff37b0116f3526cdafab18120f491323065fa345c2066e99d9ba2337bfec891a27e4ab0918d78652c329926d4186bccba63aba373b410e48301bebb03aadc8c36efed11575dd2ef1cbc5b9495dc498733dd4c7eb97c3019ec2f7a13da5c61f4130f4e89fad38b65b3bfaec1b846f70456b78869e25db44e67fbb97c6a680a9812b105c60b77074dca69c0d16a904da5388c5f935835f48e40eaf1e2da59fa2f5598b9653adf96dcaf14c68d5aadfa744fbfcb8688f195d6b8260b9f25015a58779b0ce0c00b1ef06f241effb557985c0680d131f9ba8e8f5d1bf351df45766fe4dacb08602352ac292f27e875c1adce61cc9a71bef08f567bb77e5f7586354711eb1c968bd4aa18966cbfd945b706e1cac579278aa5392da1204b1857c72b2ae68cbedf01c632e4e0aac3f88f691711d80ac557ae31ef84e69007fe74ff3195a3137f363a783418ab3cf1dc2df032e9d0ff227426b003249254b786dfa00b3a950dfd7225b6a372252e0ab276734ca6d61b86b268ca42a5aaf8134f9597ad3f6e6892d3d6aae36305cfc23c52cf519f882d61f8d43303fef819be2a11166fe83170cd28701662da9f08853b2853c09999197098cd894b0ac636192dee9d773a1d4299d93a64fe7f3b4c3897ba23652bc6635e6bbb66120e108e4d1f65ad78aa5ab3f170d7ca0e42626c279e685b3dd432f15a521c0a0e98e5fbcd0ba0d9eefcaa7b01d68b04b31c71ce9799a86f88410e84a3b61201ed738f737fd408cc284dfc7c66fef73490d4b39a0e23430833764084f60c98923296607f8c4fe4ca962118eac687c891fa444c9ccc72147f948be34b412cffcf5a8d635640388bea532cbca5cf32704c3139459d07dc2a82f77d2046008d1bd797a318b07a671879392e67e0bace396957faf2ed64393ea7ae07df1d70381b0bc3e2c70cac7c5f2274c87fac7c56bd4cd3af4297c538ec1fc4f8e862edbf759b59f052bf3aa6e87acbc2ccb78944dd82a5829000182e20381e80220a85c7858b9d76a0adb990410e518b6022325a8bd1743404b270c1ce80c9af42dd82a5828000181e203922020077e5fde35c50a9303a55009e3498a4ebedff39c42b710b730d8ec7ac7afa63e000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x3f9c875", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x293e1eb922ef1b490b4d2c4c14b8e51c383ab38933509fdee3de8a7b4b96d0dd", + "transactionPosition": 94 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001fb931", + "to": "0xff0000000000000000000000000000000012d687", + "gas": "0x18c2204", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f285058182004081820e58c081685d1d10d34edf3142ea3abfb4d9265e7ea2389b907852444da5f0952c0046f45b50589f6653be572e2590f82f26a4ada3ba4aa1637b7831390365b606352026d216bfc959d08c69ca7965439b86c03356b396062bcb98a458de9f302598960a06fdcdb4ac9ad54cecd47fffda00ea3b8a6fc666669325d25980c3ed20bfaf296f814dbc68978a23a30e03c28fc03fac83f85f9a7c23f15894643a8bef5046b4800a556ef86ff61691c7adfd35382506baf6713194c9f8a3c28be624078ba01a0037e5f8582047ab5fe94c06cdbaec8b74525a8ef23928a1bbb62440ccbcd599b10154404d690000000000000000000000000000" + }, + "result": { + "gasUsed": "0x149e229", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xa66054d53dc628a9f4132fad4beb3b4c8b77a9ecd878c31d58d9773310643125", + "transactionPosition": 95 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000235fdd", + "to": "0xff0000000000000000000000000000000001dd67", + "gas": "0x4e8c5d3", + "value": "0x1a604e782960a295", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a000a0023590780b1f46cf7da5aa8c412fdf20a3d29ffcb51e2090c60eca8ce7a6755cf86aac57d7dd57e66936da82659fbd0c263a7e9f38f3f539c4612078bb4cd042c493dbf3f240d6d2cc8716015b23e328f81bd082f1eacc23be4f00d1a36ae41db43600b4f0e95245fc967a52846d2510376596d14beea198356a3bebb4fc3cfff9b6239b25f5aa7c7ae64601deaed2be87b43a690b207bde83a79cb313f63bce1733fb94edae6bf878fa48803ef520cc8ae554ac1f34af9f55fb2717ce4d8dc9a59f1883d88c2b30a9303dbf7e63d1ba121ccb1d9105105d677a218602469cb651779b2fb8833e3a2fb949662511eb0e48ec57ad3b897e378aade2102729778bf7d9fac14e84a170aaa8a859ae05bd1617bbdee6d18e4f6742f19fcfc4d060f829cc2a19c1901d1c0a05e1ed11268b6218f9b393b29aa9c38fecabfdf11bf3781144e1e8b9aaf33b49b5ac2998cd0bcc19da4393eadf4334ab0053ee3076f4d6df1ed36fce20e00e4a735d3afc854a8752e50b7719a5feedaaa19e3067deea209e79a4bb0aee06ada2bdd419f02248ae079c7d9948c97b7d448e95326fd49a6abdf9506e3ed3a6dec1aecf431b0db2e42092f194fa9a71f648f9037c0a60ff5cab539b4e2ff47cca05d9c68b57acd0659005362fc753908599b75f2e403881bf3a17ea0550d9a1983496c891611b29dcec699c1332ff50847c22ec04a8e102f019aadbec4ef48c92879f449987188031bb4ed8cb68d0512ce2ec9204f85365793b9d9ea58941baab7823223a15805bd194ca9866d74dc3abf6ddec70633fe928518b951f39671b4d21b2c2c12500671848cbc84bb6cf98057fce0cb90f993cbc69c1e106d6195581eb8eb441a7c1502057a9f68f28ef4856c69a3643c3490129176378adfa1a297857a7f2f2ca80383c8ad7fa9969ef8bab649368a92c268b730ee01302e12604ea9644e5b0d1e640d7aca21e09f0cbedcf0eb6327a59d3e98503437ded0fc5894ef0440130f16727512ffbec3bfa4fd02753b12a0c65a040b71984bed42457725c9b2c9f494709f17fe698c157a690cf89339cc91f412109e3765bf76e6951ff2be4cf037b0463266d0bea907d01e76b1ce8e8240fdf9cff6c89baf70b232063f849885461b9d6206663e4d2833819de0187c675cb90d5bac958312fca8e926a113933f99251f8366a39fe273d1f2964c98e007acd7d15afa67ca70f8ff19f6a810ccb02a9e88991b7f54fc1b7248f5e740a6a90e362beb8a3c8bd20971ea11e3a5d5a541015b0690cca58f1e528fbe3b738e24a30e51dd775989f2dba69d89a2abe38865dde95af7a69bda1c610d2944f14de8edc0bdced10dfc0f3fd98e840f0627444e20da4b7947be112db42c1adb81230a603fa69fc0a8f3c5c87da309297a8c5d4a47eb3ee07000f81ad7b96dc27cd2639b359b08f63f79be56d41675c9af34d60f9142fd8c16d8489192ed4587cf2fc8029036d12a134b4c00c8002123b500acda1257dd1c501cf76f84c153cf5c018a3b18eeb1d63d39771938799043cd413e7b8e68b5a42d7054e609b0b5e857b878591dca7c5930fee7e313af3b8c1b48d49b3bafd6710d5ac22f13b4f63f12103bc779855772b229add4c5a21aa769fe3d0f6cad677e2d8a1c6c6da878f2cf49d4b42be7a617536bd924c93645bac6b43fef51efdccbc8679525fcaef83e89a8d41418644b2ab90813ea70a9d999f6b9a7eb233b48dc7928eda4917b07e3e0cba92674dbc8b0168d5d821d0bc093e7ba8c4c020e7a2374a96299b6f85c94a5ea467baab46b24fe798efc5e8debfd737aba2899b9bc759aec49102fa2e50915f3fbbad9ae2cb3f0ceaf287753a97330067c4e45df6da8bb01a34a6e76ccf228a28be3b6de92d158967a65898358bd47b4605ff64e320a62e4a72a394bc35df7e90cec68d8e4df0a771e3958474af79eb4d5376125e3f0b363714b9986d6985263f45f8580e05ec91e51123c868a9470fc490597432364ad04f20653b8335114b15f5c18de5e362ebc32ae2512dddfccc5c4db97e77bfeb4394ba114e2f9447c953ec89a94c39a25f8ec0bd2b0f89e3762043501aed99d6159ab4abeab32f66967fb6cab04fb56ee6ffd652095daa21642ae9083d5afec856642f9ecca82cac18df1f1fef5c868d946759af794f04319696b6fc63a83e75de58192c5f4bf21ef5e6c69a8b6afa88f4daa6c5cba2f03787b3c6cbf33421012bd9788a58dabd91d3b7769c901a46c31ed41d0459afeece50ac8de9d97c91c40c041b1173537a1f49b183ae68b6944756a63fd6000f6f881584d8e7e4863a9f3247956f108ed85a11c3b3d9e60634a62828b38322c845cd5ac80420146154d55fe1bac709803c803ece6de386d0df9d379f9ab10b82f0a29cf2d98e6ba564d2a8368508dc85c8b9876ce150fe7d88e4684723e0190c12de8200207fa88fe3af9590c8f0b19d0f0cc4ea1276779fa6815ce4294147f9189355a4dc71446560f06948889f4b7ef9b2c720260607cd9b01a69de0eec1432cafea0f87c5a1b6d0d11cbdc51c20a724238cdfd949e1221e13e8b29ea7c170eac8d9981eed96e72d59443212096d6299e286db80e661fd68bbd2d49434b90e11db7627860b17531086e59e049d5a510fb61c86c94fbc7fed1474c59280346f879698715675210bedbeab847af0e6c88d478dc20148d564fba21e557dcec0000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x9b3e40", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc209712792b9bb2ca548df72c743c2efa109181e953fac3f07e8e7026d7aa78f", + "transactionPosition": 96 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000001dd67", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x47e9165", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a0001dd671a000a0023811a045b18855820e846cbc689aebc3ec97bcdcb44da2e58c3e092c75ee715dfa353496d8d00286a582040123884cdb875d63ca4b9ceeb2f02840ac99070376e535012b6a5ed5323deee590780b1f46cf7da5aa8c412fdf20a3d29ffcb51e2090c60eca8ce7a6755cf86aac57d7dd57e66936da82659fbd0c263a7e9f38f3f539c4612078bb4cd042c493dbf3f240d6d2cc8716015b23e328f81bd082f1eacc23be4f00d1a36ae41db43600b4f0e95245fc967a52846d2510376596d14beea198356a3bebb4fc3cfff9b6239b25f5aa7c7ae64601deaed2be87b43a690b207bde83a79cb313f63bce1733fb94edae6bf878fa48803ef520cc8ae554ac1f34af9f55fb2717ce4d8dc9a59f1883d88c2b30a9303dbf7e63d1ba121ccb1d9105105d677a218602469cb651779b2fb8833e3a2fb949662511eb0e48ec57ad3b897e378aade2102729778bf7d9fac14e84a170aaa8a859ae05bd1617bbdee6d18e4f6742f19fcfc4d060f829cc2a19c1901d1c0a05e1ed11268b6218f9b393b29aa9c38fecabfdf11bf3781144e1e8b9aaf33b49b5ac2998cd0bcc19da4393eadf4334ab0053ee3076f4d6df1ed36fce20e00e4a735d3afc854a8752e50b7719a5feedaaa19e3067deea209e79a4bb0aee06ada2bdd419f02248ae079c7d9948c97b7d448e95326fd49a6abdf9506e3ed3a6dec1aecf431b0db2e42092f194fa9a71f648f9037c0a60ff5cab539b4e2ff47cca05d9c68b57acd0659005362fc753908599b75f2e403881bf3a17ea0550d9a1983496c891611b29dcec699c1332ff50847c22ec04a8e102f019aadbec4ef48c92879f449987188031bb4ed8cb68d0512ce2ec9204f85365793b9d9ea58941baab7823223a15805bd194ca9866d74dc3abf6ddec70633fe928518b951f39671b4d21b2c2c12500671848cbc84bb6cf98057fce0cb90f993cbc69c1e106d6195581eb8eb441a7c1502057a9f68f28ef4856c69a3643c3490129176378adfa1a297857a7f2f2ca80383c8ad7fa9969ef8bab649368a92c268b730ee01302e12604ea9644e5b0d1e640d7aca21e09f0cbedcf0eb6327a59d3e98503437ded0fc5894ef0440130f16727512ffbec3bfa4fd02753b12a0c65a040b71984bed42457725c9b2c9f494709f17fe698c157a690cf89339cc91f412109e3765bf76e6951ff2be4cf037b0463266d0bea907d01e76b1ce8e8240fdf9cff6c89baf70b232063f849885461b9d6206663e4d2833819de0187c675cb90d5bac958312fca8e926a113933f99251f8366a39fe273d1f2964c98e007acd7d15afa67ca70f8ff19f6a810ccb02a9e88991b7f54fc1b7248f5e740a6a90e362beb8a3c8bd20971ea11e3a5d5a541015b0690cca58f1e528fbe3b738e24a30e51dd775989f2dba69d89a2abe38865dde95af7a69bda1c610d2944f14de8edc0bdced10dfc0f3fd98e840f0627444e20da4b7947be112db42c1adb81230a603fa69fc0a8f3c5c87da309297a8c5d4a47eb3ee07000f81ad7b96dc27cd2639b359b08f63f79be56d41675c9af34d60f9142fd8c16d8489192ed4587cf2fc8029036d12a134b4c00c8002123b500acda1257dd1c501cf76f84c153cf5c018a3b18eeb1d63d39771938799043cd413e7b8e68b5a42d7054e609b0b5e857b878591dca7c5930fee7e313af3b8c1b48d49b3bafd6710d5ac22f13b4f63f12103bc779855772b229add4c5a21aa769fe3d0f6cad677e2d8a1c6c6da878f2cf49d4b42be7a617536bd924c93645bac6b43fef51efdccbc8679525fcaef83e89a8d41418644b2ab90813ea70a9d999f6b9a7eb233b48dc7928eda4917b07e3e0cba92674dbc8b0168d5d821d0bc093e7ba8c4c020e7a2374a96299b6f85c94a5ea467baab46b24fe798efc5e8debfd737aba2899b9bc759aec49102fa2e50915f3fbbad9ae2cb3f0ceaf287753a97330067c4e45df6da8bb01a34a6e76ccf228a28be3b6de92d158967a65898358bd47b4605ff64e320a62e4a72a394bc35df7e90cec68d8e4df0a771e3958474af79eb4d5376125e3f0b363714b9986d6985263f45f8580e05ec91e51123c868a9470fc490597432364ad04f20653b8335114b15f5c18de5e362ebc32ae2512dddfccc5c4db97e77bfeb4394ba114e2f9447c953ec89a94c39a25f8ec0bd2b0f89e3762043501aed99d6159ab4abeab32f66967fb6cab04fb56ee6ffd652095daa21642ae9083d5afec856642f9ecca82cac18df1f1fef5c868d946759af794f04319696b6fc63a83e75de58192c5f4bf21ef5e6c69a8b6afa88f4daa6c5cba2f03787b3c6cbf33421012bd9788a58dabd91d3b7769c901a46c31ed41d0459afeece50ac8de9d97c91c40c041b1173537a1f49b183ae68b6944756a63fd6000f6f881584d8e7e4863a9f3247956f108ed85a11c3b3d9e60634a62828b38322c845cd5ac80420146154d55fe1bac709803c803ece6de386d0df9d379f9ab10b82f0a29cf2d98e6ba564d2a8368508dc85c8b9876ce150fe7d88e4684723e0190c12de8200207fa88fe3af9590c8f0b19d0f0cc4ea1276779fa6815ce4294147f9189355a4dc71446560f06948889f4b7ef9b2c720260607cd9b01a69de0eec1432cafea0f87c5a1b6d0d11cbdc51c20a724238cdfd949e1221e13e8b29ea7c170eac8d9981eed96e72d59443212096d6299e286db80e661fd68bbd2d49434b90e11db7627860b17531086e59e049d5a510fb61c86c94fbc7fed1474c59280346f879698715675210bedbeab847af0e6c88d478dc20148d564fba21e557dcecd82a5829000182e20381e802209e50cb8175ce18db05fdce8f8651f33386cf196129b4be40a075c99187914609d82a5828000181e203922020af0d7d76927d5d2e47081e7d3d428177f43834c9386416c54fca266b3444f21700000000000000000000000000" + }, + "result": { + "gasUsed": "0x31bebc8", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xc209712792b9bb2ca548df72c743c2efa109181e953fac3f07e8e7026d7aa78f", + "transactionPosition": 96 + }, + { + "type": "call", + "subtraces": 7, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000001bac42", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x1273e026", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000eb8181828bd82a5828000181e20392202056bc2504599e6513bd273965038a95a26005013ceb8a54569a0046a513b162371b0000000800000000f555010f29c20971f6e29cead00b57277fb5975fd83f914400a29f74783b6261666b726569686572366e326c6f657735777166783777797768616e63347a74346c6a7776703263356f62727968346a756f356a6371616637651a003814d61a004f81164048001b6a51c29fe8e04058420192040362a715acc12b1e0985cf7fa25f4935d562a4e79f1699945b56c51e1cd242a0363d28cb1f9d735816b771f086829db7b4c88fc94f03a4814cfb597a9d1601000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x8e9f7bd", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000982811a045b576b410c0000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff000000000000000000000000000000001d0fa2", + "gas": "0x12604c76", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000014c1cb970000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000054400c2d86e000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x13301e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x124c779f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x123babbf", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 3 + ], + "action": { + "callType": "staticcall", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000021f2a6", + "gas": "0x1228a5ee", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000009d8b06780000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000ea82584192040362a715acc12b1e0985cf7fa25f4935d562a4e79f1699945b56c51e1cd242a0363d28cb1f9d735816b771f086829db7b4c88fc94f03a4814cfb597a9d160158a48bd82a5828000181e20392202056bc2504599e6513bd273965038a95a26005013ceb8a54569a0046a513b162371b0000000800000000f555010f29c20971f6e29cead00b57277fb5975fd83f914400a29f74783b6261666b726569686572366e326c6f657735777166783777797768616e63347a74346c6a7776703263356f62727968346a756f356a6371616637651a003814d61a004f81164048001b6a51c29fe8e04000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2e9853", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001f500000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 4 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000007", + "gas": "0x111c5e8e", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000c26ddbd50000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000064500a6e587010000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2ce23f", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000104f00120654f8b6172b36ea1e89d8000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [ + 5 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff00000000000000000000000000000000000007", + "gas": "0x10ece94f", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000d7d4deed000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000067844500a6e587014200064d006f05b59d3b20000000000000584d8281861a001d0fa2d82a5828000181e20392202056bc2504599e6513bd273965038a95a26005013ceb8a54569a0046a513b162371b00000008000000001a00176c401a001b60c01a003814d68000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x378d1f2", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043844f001205e5f30079f016ea1e89d80000500006a8d5434e2fdfc905110000000000520002f050b9c8c93d441ca1915680000000004d83820180820080811a034d18bd0000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 5, + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000007", + "to": "0xff00000000000000000000000000000000000006", + "gas": "0xddc0709", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000de180de300000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006e821a85223bdf5866861a0021f2a606054d006f05b59d3b20000000000000584d8281861a001d0fa2d82a5828000181e20392202056bc2504599e6513bd273965038a95a26005013ceb8a54569a0046a513b162371b00000008000000001a00176c401a001b60c01a003814d68040000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x2807281", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000d83820180820080811a034d18bd00000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 6 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000000005", + "to": "0xff0000000000000000000000000000000021f2a6", + "gas": "0x311f73c", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000f98c996600000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000009c8258948bd82a5828000181e20392202056bc2504599e6513bd273965038a95a26005013ceb8a54569a0046a513b162371b0000000800000000f54500a6e587014400a29f74783b6261666b726569686572366e326c6f657735777166783777797768616e63347a74346c6a7776703263356f62727968346a756f356a6371616637651a003814d61a004f81164048001b6a51c29fe8e0401a045b576b00000000" + }, + "result": { + "gasUsed": "0x9858a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x1fc5fcce6d413a9fd7bd827fd070aeeec4762d054b583f5633da53347117a8b4", + "transactionPosition": 97 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce389", + "to": "0xff000000000000000000000000000000002ce3be", + "gas": "0x41499d9", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782193e18590780912ab6e26f928bffa3c68c9a467677eec142ecf7a2d7c57711f54aa9bebf313780e8f80cc5646a351ad95444b99ed0dca55ae5fad63d8694ef89818973ae10a1aeb4b104ae7f4803b0465d687d24c4311ff1a29125137f091a1f384e83c6dd6f0f31df5cea648ebaf3bf23e192e5570a56c6146a1a8b60124d047a11b0985d11ad2611bdf7042e2cd719dd55845779cc93293d56ec0c121918b2a2f69de6f516102859fea9b3e7aae7ccbc556e73015f404fbe8f2a6084a013f1b746d69c755b8533c06e199c3a3cd991793311106e59a79f319b007afbc7ec2b8cb93f9a42f14f2e5e7f39e26aa6b677313eca6a1f00ab0ce08d0eb91c0f1c9fad8069012697d54aa1ec9caaedad169001eaa0665504405c765b25f4d1209bb69b2f4b3f52390f1e0235828501c87f43b5bae1f51d54c5bb5e513d33441385086b40c08b2787d88350ef8b2e4e26ae68eb77d6e47c91b19bf6180433e342c57b7128d755a3bb1508a42134043ff697ca4915e666cb1e98ee0176480144a92c4d2a239e16343090965542b9073d87d453707027cd41b3cc4bedf13cd490a355e4da51322757aeb1c42aa193a7c848e55ab81a6e43696084f9c5965d467b8b797db16b4cdd8f15576a7f2efc5ec18c356c7a079edaca1be4b842a820c2776049f49c3e237a6f8d096f7473ca10b1b419adbce98aa92d0d34e7926d463166bf2b7afdaad927c6649d3b719a14db07e358a9b817ba1df393813cea360cf3754a12449716490218ec147c01f394fd5a78e1fd1efd1825f74b6c5ca1640b328ad5b7078f3d00b8988f85acb0bd284a1515d31b7daf53f780c22179fceafcbbef9e4cf3e99c4028671aba7c2adfcf2b455e914f962705f1c96e97042c1eedc665844f2bdf90159975255aaef853a9198cbfcbfdd982ea59ce4ccf92b9463035534c450743eafa9148eb11fafb866c07d79d4cdc3ae79ed0f2b95579adbd9ed25fa3d18a9a3ed4c936a47c9d670cc583a98d71490c569bc4637f93deea73cfa34ed7fd465c93e714b33431255c3300d0d03055df972b067d19bacbfe7fc79b25db29ac9718f188ec0421b2defd12c408a48c5d33bbc655fcce869995f33f9fd53f3dc36a73b7a362430bca272639b40d134e1d7405bdd8983c7bac2d8fb7208c6219b0899ae317015eaebcceee8fc9b93d573cbe70d378e50ba9a969f4a69407524a4023ef97aef3c01d128576f63b5699cb5c691a7039c4ef3ca9af3baf713495f149d7425cb417c52052f3e530e1e438dcf8b627f0263a27caa010917169b91952820d1a46a4b4f54357fda87463a5c866ce6afe7d9b23eefe2e173d737fe492470fa481ab883e0a82b86637364dcdc03c47318423f7f512839510ad64b82dd392b5d0c6acc2b824a9727a7717819dd8aba714e2eb11bf32348c632f06d66ab968e070ee328e357c90db353536eb2a2edca946abafd2f26b6f9ad6096fb44894d5a4d50e573d9c1c2d0fb3bfa5ec202c581b1cc08fd7594ebe6892ce53457fd7268bac26471341a438f811c9130d5397d61a98351e79d64ca387ede4e93eeaced4a56528a04f1299d9086887b2c726eb504e111af2445ed0aa76363a0ee535be1dd027a7adb43426caa0d90cac9116b5c36d120430ca5dae1c98644b72c6f847d74a0e71ae69604a4a4bab5628fdac4f92aaa86c70c4533334b366a751f23473cf65616aea0661fdbee4b9d3f2cb82578f44703e8ec514a72ce9b299e617d37106dd23f4c53dbe27780dcdb0312007fb60c2d43b3257ac99e75b71cbb611ebfc72c1fb724b15a8fe58b2ddcc2a5cd976a15ce41b4dd09cb9d09054e835d6d60b0172bf904074853ca9c6613c0fae8f792586238416b054c8710fd387e4d46c229a6d0d8f9b653bac5386284710668bb3fca857163f6db48e0420f0087dcb0dedde46fe29a90641b6c3c49db5d351a267305eeaea95c6e62e16a91917b7816972524fea118f26bf851baf069b8ecf2081cf91451d697be0b6887bccf0a3b67c96d44d1bff69cef9546d08e8dd66cb7417e7a2723440a6b6442c2a58bece8cba21ab9f197c3d0bc74c781e30efd25cf2542639ac3acfabdea3f7af221ad28c797930d368076cbcd112c0a1bf198d0c081d7f7f216889bcb5b446d720fa7f1333fba0441216568b55b9e3a869c47afaabdeb282b65c774c98db3d00c00406276b12d86c44f8dd8970bd7dc738bbe1ad712a10137ed87968c0f0c8afc53ba3f9a070e700dcd22b47340d17c2c7b2f1cd2039c89334e722e25b7d619c8319a6727f4b68279c64538ce4c03318853f4e1944b8d81d8b8007aca927ffbc02e69a77c45d2632a7c0afcaa90f5e6f2736f58bc304dd1170f685e8b47fccaded0661255d6ab8a290e4a8c44dedcf3c9660a7d35f040f0d63e14bde335b7968d5ed6e984a892b0a6a8139d96eb54a8d1022e8db082e062e0c252c3e01d6403afab8aaf6bc6f24d8157ed6cecad155e259ad5b018db69ccc08c0f15811ea78917fb72ad9e9573c7645fcf59cfa531b078ac8fe5b51efb61d8c1343ec3d890f34d6095d6000f0be39e9d0dfeb532f2708168b75f28ceddb56aa4581da4d5aca0b39a456ea7b8e43e883c9d3b12d51c0826ccb69d1e77956afd2adfc115a1e44ad78e68c2b93b61d71e0ddd4cdaaa7fd3589a1b9d2a42df325724d181b7c96af26643d47603af1289ede8f54c634ddb400000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x604a1e", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x2fcb2ae181befb6e530f111ad25c983e1d2e208d11caa73145c811266bdf34bf", + "transactionPosition": 98 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce3be", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3e52cf6", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008318808821a002ce3be193e18811a0453643c58200b7238a7ac085ec5b663a2ac9babde2c59068dd6e6e61e57784fee049b0eab2058201015e3ffb83aba7ad6eed57e89a09176c78e97da5c95b02f947c84ec8868be3e590780912ab6e26f928bffa3c68c9a467677eec142ecf7a2d7c57711f54aa9bebf313780e8f80cc5646a351ad95444b99ed0dca55ae5fad63d8694ef89818973ae10a1aeb4b104ae7f4803b0465d687d24c4311ff1a29125137f091a1f384e83c6dd6f0f31df5cea648ebaf3bf23e192e5570a56c6146a1a8b60124d047a11b0985d11ad2611bdf7042e2cd719dd55845779cc93293d56ec0c121918b2a2f69de6f516102859fea9b3e7aae7ccbc556e73015f404fbe8f2a6084a013f1b746d69c755b8533c06e199c3a3cd991793311106e59a79f319b007afbc7ec2b8cb93f9a42f14f2e5e7f39e26aa6b677313eca6a1f00ab0ce08d0eb91c0f1c9fad8069012697d54aa1ec9caaedad169001eaa0665504405c765b25f4d1209bb69b2f4b3f52390f1e0235828501c87f43b5bae1f51d54c5bb5e513d33441385086b40c08b2787d88350ef8b2e4e26ae68eb77d6e47c91b19bf6180433e342c57b7128d755a3bb1508a42134043ff697ca4915e666cb1e98ee0176480144a92c4d2a239e16343090965542b9073d87d453707027cd41b3cc4bedf13cd490a355e4da51322757aeb1c42aa193a7c848e55ab81a6e43696084f9c5965d467b8b797db16b4cdd8f15576a7f2efc5ec18c356c7a079edaca1be4b842a820c2776049f49c3e237a6f8d096f7473ca10b1b419adbce98aa92d0d34e7926d463166bf2b7afdaad927c6649d3b719a14db07e358a9b817ba1df393813cea360cf3754a12449716490218ec147c01f394fd5a78e1fd1efd1825f74b6c5ca1640b328ad5b7078f3d00b8988f85acb0bd284a1515d31b7daf53f780c22179fceafcbbef9e4cf3e99c4028671aba7c2adfcf2b455e914f962705f1c96e97042c1eedc665844f2bdf90159975255aaef853a9198cbfcbfdd982ea59ce4ccf92b9463035534c450743eafa9148eb11fafb866c07d79d4cdc3ae79ed0f2b95579adbd9ed25fa3d18a9a3ed4c936a47c9d670cc583a98d71490c569bc4637f93deea73cfa34ed7fd465c93e714b33431255c3300d0d03055df972b067d19bacbfe7fc79b25db29ac9718f188ec0421b2defd12c408a48c5d33bbc655fcce869995f33f9fd53f3dc36a73b7a362430bca272639b40d134e1d7405bdd8983c7bac2d8fb7208c6219b0899ae317015eaebcceee8fc9b93d573cbe70d378e50ba9a969f4a69407524a4023ef97aef3c01d128576f63b5699cb5c691a7039c4ef3ca9af3baf713495f149d7425cb417c52052f3e530e1e438dcf8b627f0263a27caa010917169b91952820d1a46a4b4f54357fda87463a5c866ce6afe7d9b23eefe2e173d737fe492470fa481ab883e0a82b86637364dcdc03c47318423f7f512839510ad64b82dd392b5d0c6acc2b824a9727a7717819dd8aba714e2eb11bf32348c632f06d66ab968e070ee328e357c90db353536eb2a2edca946abafd2f26b6f9ad6096fb44894d5a4d50e573d9c1c2d0fb3bfa5ec202c581b1cc08fd7594ebe6892ce53457fd7268bac26471341a438f811c9130d5397d61a98351e79d64ca387ede4e93eeaced4a56528a04f1299d9086887b2c726eb504e111af2445ed0aa76363a0ee535be1dd027a7adb43426caa0d90cac9116b5c36d120430ca5dae1c98644b72c6f847d74a0e71ae69604a4a4bab5628fdac4f92aaa86c70c4533334b366a751f23473cf65616aea0661fdbee4b9d3f2cb82578f44703e8ec514a72ce9b299e617d37106dd23f4c53dbe27780dcdb0312007fb60c2d43b3257ac99e75b71cbb611ebfc72c1fb724b15a8fe58b2ddcc2a5cd976a15ce41b4dd09cb9d09054e835d6d60b0172bf904074853ca9c6613c0fae8f792586238416b054c8710fd387e4d46c229a6d0d8f9b653bac5386284710668bb3fca857163f6db48e0420f0087dcb0dedde46fe29a90641b6c3c49db5d351a267305eeaea95c6e62e16a91917b7816972524fea118f26bf851baf069b8ecf2081cf91451d697be0b6887bccf0a3b67c96d44d1bff69cef9546d08e8dd66cb7417e7a2723440a6b6442c2a58bece8cba21ab9f197c3d0bc74c781e30efd25cf2542639ac3acfabdea3f7af221ad28c797930d368076cbcd112c0a1bf198d0c081d7f7f216889bcb5b446d720fa7f1333fba0441216568b55b9e3a869c47afaabdeb282b65c774c98db3d00c00406276b12d86c44f8dd8970bd7dc738bbe1ad712a10137ed87968c0f0c8afc53ba3f9a070e700dcd22b47340d17c2c7b2f1cd2039c89334e722e25b7d619c8319a6727f4b68279c64538ce4c03318853f4e1944b8d81d8b8007aca927ffbc02e69a77c45d2632a7c0afcaa90f5e6f2736f58bc304dd1170f685e8b47fccaded0661255d6ab8a290e4a8c44dedcf3c9660a7d35f040f0d63e14bde335b7968d5ed6e984a892b0a6a8139d96eb54a8d1022e8db082e062e0c252c3e01d6403afab8aaf6bc6f24d8157ed6cecad155e259ad5b018db69ccc08c0f15811ea78917fb72ad9e9573c7645fcf59cfa531b078ac8fe5b51efb61d8c1343ec3d890f34d6095d6000f0be39e9d0dfeb532f2708168b75f28ceddb56aa4581da4d5aca0b39a456ea7b8e43e883c9d3b12d51c0826ccb69d1e77956afd2adfc115a1e44ad78e68c2b93b61d71e0ddd4cdaaa7fd3589a1b9d2a42df325724d181b7c96af26643d47603af1289ede8f54c634ddb4d82a5829000182e20381e802202f151b4c0c83a349d0df400c799410b60872287923ed67236c6fded247435711d82a5828000181e2039220200b139cb28169e7bf96a3e893f06114798319b67738e3f1d3e25c15251fc05809000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x31d48a6", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x2fcb2ae181befb6e530f111ad25c983e1d2e208d11caa73145c811266bdf34bf", + "transactionPosition": 98 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000194d9c", + "to": "0xff0000000000000000000000000000000018b644", + "gas": "0x420cf8b", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000078782197cae590780af74af52a5e7cfc11112a58658f3464000126bb0bdbe9cac881aab7373a5b01444a96ce7cac4da6cab7931ad1302d103ad1a909beafb5e6da3a12dc834107ad467b58db1d330b54187c5e76133ce0dfae5286e16dc77f21200b431311385ccae08f31082cb5fe315e2f03763339b2a8e7f5d4aa22394ab9b1bd2e5a2ee9f457878d7922cc963fb7c9df63dac57bd3bd1b4e474c042691e7691229c7832cd385e5dcf57d63f89f56b417d6a0454f6f7ebe10ddafec855583091e9f0a182fc8efeb873b773a6a2a5ec2f78424c1b274d629653ab7f501ca8e153b5bd133bfbd4875b708ab19a70dba7ade20d6c711e228a96a87a7b889c690cab7afe89b65bf75a1320383fe2575e886a9f2e540c73c683ebcf237a7e9249e1e79897026f5410211837ebfbd01106ada77feb2fddd1594668877f93c92007f819e2958174cfee14c3a9f19e0cb82184ca9a0bcbad69dc7fb54baed0248d1ab6523a03a483b8894ff90a82f720b05cda51ba3473d78070ef62dfcb4db8381b8bf00fce3b1e47c599b29c4e4f3d3537cf32cb0b414a43b05ecac3008bee1e41f7b0e434330b144ca8943d0eb355a21800b489c8747a8df65799ce85c5278b8af057ecaa0094ecc3b7125326e168bb9e3a6840305c138f04e8ac6c9833050e67d2f5b5bae0b82e704f0e478ad5e249af7c71e5f813f4e2762789e22db59bd998a600a96f12ebb4cccbbc0167250e5068db6097b417820193ffb61b14ed1decd0a4c4ce4aa22e0a4b4345b112cc70fbe58e39b55d8658d0293f2e0553fb9296203aab2a8e3921bf12e3abc25b8a68a82b8a32f780d643706c2fdd95aa83e0f80a1148ecdeee6d82f68b7d0970d26535d0f8a45ecbe83ff1736890aee1ca3b2c3c16c04b7a79be725e27a83d2a650c953c7618359ff8248c341387fe194553c7aa2d7c36b90427b78bb9011137de00fa755df244a478ef86182e1154bcb9704693d2a4b36bbef19817ca6fba18738f3ab605231ca40458590ce8a25215e86e4fded86922730312c0e1a9fce06819f308e197e58cc9df7505ef779f011945960debf96779a0d3b4853865953aac108b3d10cbece20950a0612147e65edc8a59aaf7c8ca518f9fdb452b316f4ebf3fd18664b692cf0195b2f66467b9b8c398e118c1e90d45be22969e12a226d9c51d1f5e437de700096f7bca6b5b52b72ea04b89828b0d5d2f2349b11e35150b53c844616524804b13ff6e50bcc2cee62a91bac6c97ef5601a4c6d26b70e3ae78c18b41560995ed358bf8bce8a40aa30218576eab0068c6f4d7a9f16524830fe8418d98fe8475a1ca81174b85c84e70944a47cb840aab45b22036cf0a428b3f42ae6f5b44a9f9cf8a8e160659c887db1eb574690f261328c2d7d61d77e2571823a0e071b541b25779deda2b99aee9712bb5f78d30b6924e67a466cf6e5cee885fd7dd1cbe54e19a0acce5a4ec125b4191402fec6f7cd9e6b74f693b6e1a11552aa1d0c74db96db59cecd8165540ed97de16ae68161ffd0004e46e94db01f6e210ba0befbcc545c75f26f63f6a5d4b4c3390118c4f9d250a816bb83b162ae29dfe7f43161f5383e6a6514a1dfe089af68a6050d8628af0ae8ed721ac2eaf797d0a8291139c8c4f0803ad4d1e1476d641f65ce498b78f28e779aaa3b509105e295b178789c0a88a002a698c32d6500900bc46da2152ba8b48e1adb116603a56ac12af02a647547f6bab816ce8b90909c28b88be764c8534344fea5b4a98d6d13427c0a1a8b3479fd6ab5fcd298c5bbbd53073ac3162a1d71573b03bed5fac16fa8180dbd516bdeb05f8194fba24014950634092426f4a3894967e29fa3911776d59c2c96fc592c73e90542ae654a087a5773045e35dabf4607f78cd01df0c2b14687a7f041dfb54ea9b2edc2e13c91df1d99fab8354d8ac2d7397c332c05573deb2482054e35f750e6b36c337abf0292a2b7dacc7ca94a1b8aae9f2d2355a79fd807e28fa6b7a05ce0bf4246422361a8f92597f47ac6d0536fd34a0e43ce101097d99bb0ca17255046025305e2847ed242c64e86e131b71de29848e8a731dbbf72fc21df1ef705064a4fec6e8dcc64a2e80de3013a9969563667fa312044c74bb6fe0b548f01a00434d80d3f16ee2f0023995c758050f2023e44cdc2d409b98ca630e47e0889e322adff03315a959e517b3ea1bec48e754c7507ffcb6aab665673033a950ea575006f67fe715a380f8c157d40b5a8cb1c9feca96555c1993672a737f13a375700ff5402e77440163eb33d751e7395011d6ec56bad25a7471a154199067a029e6a456b64728fb5551c75d578e6fa7d4c208a5d5121c8068ccbad10d37097f4eaa65ab1fe0aaaf4a246a938d6532465c9ee33568fc97829bc3e8ebe41006890cea32107e374d877f5145541d6c42c9565835f304b072dd680f7b28e0636c37c77c83cf94c7995ed2dd6e3de65145100e31530df58cf3c033e273d8dd958d04c24b0b4d025dddd658c91ac7d9a533d5cfdbd053b222df4fbfbef900da6a930ae232efa070958c210dcc655d513504248ed405924bc76e35ccdba1787732ff9f074899c01a79af782c9ad343869a7b3cde3812d6c51d453469a99f81b1eaca9e1eb1cf672dff039f9790cb72cfc70e23b1810a2ff1fad6f18e0bd7e45b7669f206444383f572c31555dc9aade67df6a23e7f458b47041d2185d4300000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x6c75d9", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0e60536ae59f9e7056602af5758917ceb7b122029a30d36b81f755578da9b6f9", + "transactionPosition": 99 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff0000000000000000000000000000000018b644", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3e5247f", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000082c8808821a0018b644197cae805820459182f6c450d64f4ce1b2cdd1f1b72010f8d595fd9303f5cea43b716af1082a58203982169b56bf07db8724ad2c00f8855d497d4eee8ca4ad3562991728ee37cfd7590780af74af52a5e7cfc11112a58658f3464000126bb0bdbe9cac881aab7373a5b01444a96ce7cac4da6cab7931ad1302d103ad1a909beafb5e6da3a12dc834107ad467b58db1d330b54187c5e76133ce0dfae5286e16dc77f21200b431311385ccae08f31082cb5fe315e2f03763339b2a8e7f5d4aa22394ab9b1bd2e5a2ee9f457878d7922cc963fb7c9df63dac57bd3bd1b4e474c042691e7691229c7832cd385e5dcf57d63f89f56b417d6a0454f6f7ebe10ddafec855583091e9f0a182fc8efeb873b773a6a2a5ec2f78424c1b274d629653ab7f501ca8e153b5bd133bfbd4875b708ab19a70dba7ade20d6c711e228a96a87a7b889c690cab7afe89b65bf75a1320383fe2575e886a9f2e540c73c683ebcf237a7e9249e1e79897026f5410211837ebfbd01106ada77feb2fddd1594668877f93c92007f819e2958174cfee14c3a9f19e0cb82184ca9a0bcbad69dc7fb54baed0248d1ab6523a03a483b8894ff90a82f720b05cda51ba3473d78070ef62dfcb4db8381b8bf00fce3b1e47c599b29c4e4f3d3537cf32cb0b414a43b05ecac3008bee1e41f7b0e434330b144ca8943d0eb355a21800b489c8747a8df65799ce85c5278b8af057ecaa0094ecc3b7125326e168bb9e3a6840305c138f04e8ac6c9833050e67d2f5b5bae0b82e704f0e478ad5e249af7c71e5f813f4e2762789e22db59bd998a600a96f12ebb4cccbbc0167250e5068db6097b417820193ffb61b14ed1decd0a4c4ce4aa22e0a4b4345b112cc70fbe58e39b55d8658d0293f2e0553fb9296203aab2a8e3921bf12e3abc25b8a68a82b8a32f780d643706c2fdd95aa83e0f80a1148ecdeee6d82f68b7d0970d26535d0f8a45ecbe83ff1736890aee1ca3b2c3c16c04b7a79be725e27a83d2a650c953c7618359ff8248c341387fe194553c7aa2d7c36b90427b78bb9011137de00fa755df244a478ef86182e1154bcb9704693d2a4b36bbef19817ca6fba18738f3ab605231ca40458590ce8a25215e86e4fded86922730312c0e1a9fce06819f308e197e58cc9df7505ef779f011945960debf96779a0d3b4853865953aac108b3d10cbece20950a0612147e65edc8a59aaf7c8ca518f9fdb452b316f4ebf3fd18664b692cf0195b2f66467b9b8c398e118c1e90d45be22969e12a226d9c51d1f5e437de700096f7bca6b5b52b72ea04b89828b0d5d2f2349b11e35150b53c844616524804b13ff6e50bcc2cee62a91bac6c97ef5601a4c6d26b70e3ae78c18b41560995ed358bf8bce8a40aa30218576eab0068c6f4d7a9f16524830fe8418d98fe8475a1ca81174b85c84e70944a47cb840aab45b22036cf0a428b3f42ae6f5b44a9f9cf8a8e160659c887db1eb574690f261328c2d7d61d77e2571823a0e071b541b25779deda2b99aee9712bb5f78d30b6924e67a466cf6e5cee885fd7dd1cbe54e19a0acce5a4ec125b4191402fec6f7cd9e6b74f693b6e1a11552aa1d0c74db96db59cecd8165540ed97de16ae68161ffd0004e46e94db01f6e210ba0befbcc545c75f26f63f6a5d4b4c3390118c4f9d250a816bb83b162ae29dfe7f43161f5383e6a6514a1dfe089af68a6050d8628af0ae8ed721ac2eaf797d0a8291139c8c4f0803ad4d1e1476d641f65ce498b78f28e779aaa3b509105e295b178789c0a88a002a698c32d6500900bc46da2152ba8b48e1adb116603a56ac12af02a647547f6bab816ce8b90909c28b88be764c8534344fea5b4a98d6d13427c0a1a8b3479fd6ab5fcd298c5bbbd53073ac3162a1d71573b03bed5fac16fa8180dbd516bdeb05f8194fba24014950634092426f4a3894967e29fa3911776d59c2c96fc592c73e90542ae654a087a5773045e35dabf4607f78cd01df0c2b14687a7f041dfb54ea9b2edc2e13c91df1d99fab8354d8ac2d7397c332c05573deb2482054e35f750e6b36c337abf0292a2b7dacc7ca94a1b8aae9f2d2355a79fd807e28fa6b7a05ce0bf4246422361a8f92597f47ac6d0536fd34a0e43ce101097d99bb0ca17255046025305e2847ed242c64e86e131b71de29848e8a731dbbf72fc21df1ef705064a4fec6e8dcc64a2e80de3013a9969563667fa312044c74bb6fe0b548f01a00434d80d3f16ee2f0023995c758050f2023e44cdc2d409b98ca630e47e0889e322adff03315a959e517b3ea1bec48e754c7507ffcb6aab665673033a950ea575006f67fe715a380f8c157d40b5a8cb1c9feca96555c1993672a737f13a375700ff5402e77440163eb33d751e7395011d6ec56bad25a7471a154199067a029e6a456b64728fb5551c75d578e6fa7d4c208a5d5121c8068ccbad10d37097f4eaa65ab1fe0aaaf4a246a938d6532465c9ee33568fc97829bc3e8ebe41006890cea32107e374d877f5145541d6c42c9565835f304b072dd680f7b28e0636c37c77c83cf94c7995ed2dd6e3de65145100e31530df58cf3c033e273d8dd958d04c24b0b4d025dddd658c91ac7d9a533d5cfdbd053b222df4fbfbef900da6a930ae232efa070958c210dcc655d513504248ed405924bc76e35ccdba1787732ff9f074899c01a79af782c9ad343869a7b3cde3812d6c51d453469a99f81b1eaca9e1eb1cf672dff039f9790cb72cfc70e23b1810a2ff1fad6f18e0bd7e45b7669f206444383f572c31555dc9aade67df6a23e7f458b47041d2185d43d82a5829000182e20381e802200b3169218eaabe0a254cadd20d2eac54c4e57c1839c602b8ed7a1fb2bcfa0e5fd82a5828000181e203922020077e5fde35c50a9303a55009e3498a4ebedff39c42b710b730d8ec7ac7afa63e0000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x31f607f", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0e60536ae59f9e7056602af5758917ceb7b122029a30d36b81f755578da9b6f9", + "transactionPosition": 99 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002d44e1", + "to": "0xff00000000000000000000000000000000116ff5", + "gas": "0x501f15", + "value": "0xd015638ef2350000", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x12c553", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdf41cbcc8c905708453c4636c5b0ebf709ca0464982158a996000a244a3dcd13", + "transactionPosition": 100 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000241537", + "to": "0xff000000000000000000000000000000002d44d5", + "gas": "0xbbacb", + "value": "0xce8869a46a946e435", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x12c03f", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xd0e4426b5949939e81e1aa223c77a6d1fa37d7687e32f319ad9dbfcf608128e6", + "transactionPosition": 101 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0x53bdfdea92f7a60aef82228926d02878018acb4e", + "to": "0x8460766edc62b525fc1fa4d628fc79229dc73031", + "gas": "0x6b64a5", + "value": "0x0", + "input": "0x5535dbf60000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003b6261667962656965677273376934636a6e6a767072666a356d67653367747076767a6261796e3672657a6b6433666a36746a6a70797763707a68690000000000" + }, + "result": { + "gasUsed": "0x6143fc", + "output": "0x000000000000000000000000000000000000000000000000000000000000043e" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xf73074b893cce66d2f798bc0862e12501da242db7004d3bd4aab36d7e0fcbc91", + "transactionPosition": 102 + }, + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000172b9c", + "to": "0xff00000000000000000000000000000000172b21", + "gas": "0x5205362", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000789821a000a651a5907808fd515f991b4f6a33de58875255e78be7d80628951a6f23f375280c3ec01e5aac19d252b2f8ddbe279ddb5758c96dea3b5be4a3f34581d5eba0e152cf8e649c5c272759b0b268aaaf09336e5ef9a9d0eea3b4d6e346f16370709411194f1274f03d6160a838e2d85e954e0070d434f05033a791cd1e0c149df1dab881b11423d43851c40be685faca80d7cc78959beb3975f8c49164ca96c11a2ede913c5da8cf168bd6407080e794bbac843e44f84c57b987103da83c6a9761c4dc00dbfa072b64ba1914e5588853cc3975f3463023dc27f04d6294ccf24b512c371066f3dc102db9c89172683d405c605e81488a03184c0b2996e963bc2297813598f82fa0ebdf4a13e95284e8807ffbb81573b8e73d495d18a53b9a25e39e8ccf36a50078101f9cbb2c68ad953c9e98e99dea30d8f65b821286709adcd4b99754a696e4f782e8715ac7458dda55863e46c87c10365a36bad7db606b42f49dad977252ba8fc2b615a9a68b6e4c5db36af1959788c9c01f6c42e30aec6b9618d475723feee7985d8d8a0119154f3a0cc0b44cfa94acb31e8d34a1eb9db44d389f5e6b840c80997fa17428581ce07cebe0e5a72a57d8b8df76fe7162c5fcd22771f6f39e7f09c420c298016be276d70715ad584b7e27545258c0c082f9df2a642e3f27b85db410aa8cd1e46b323b673d27f93c0aa1508e387d86a474565af8540c5912201af661ad35a7ccf0b7557d3047e6c467afce9b99ef937a026f17c63b6c56cfa2c075555e687959e1ac7d4ef164401b2dab427c4b8925eb4961db305123f932d15517b91d87218e46cad09696eb1b70f82d588c8d52d920ed13bf990f469bbe15e95c04402738842f4690e0d6963eaca9a214e9539f274424752c6855cd172f134fee67c397179ebabc8c67d87ac3862e9cb38b609aadfc476aa45acefeeaf1dde8e1d12cf3be18a24904f5261c36912844abbc6f986be2c730731556cb83ba4fb0a2e1c1f61836b805b0b404ef458aaee2ec5901f50c87c245772d68a062988be6c0178910957cd03a46d9d60cb3c6522a03e84fa2c4e752bffdf0faa82a1fe17374287456fedd11dfd4ea22163d1b3e0d4bd7c84ee73f63f8f6511ecd3297591ceb9bb59a6a52ca4d9316fa852ca949b496e8bc3ababaa8256ce13c413ff4c863fac0abb195ed523fc7ac26750864b33dcbfeb53febbd388ce3cf2cc541d9be71a8f0aa7ea56e806597ca6d08a4b763a68a1d8be498a1bec737872fe8ef18db169071932627a8f9192a498d2cb6a73ac23b3b88a0b1d3bfa271ce50abbe7d1b60cc1b9c0525b7d39daa328b854390c1538a69b6e73168143378d540cf1f11236bf4896da263ea751fe44d06031f0d65d52c735cfe0cbe82809adc1e09691e75a14cfb7c6e8f120544525c0ead12a3471bcfe8bf9aa3555c13c12a07ae2aae1ddf970b9cf714cfad51ff458228e051e00f2b46e1ef86870b6085b8e5820aaac1543cf1348d1fe21bb39dbe6352b52641688b2c2c666c8eba27394edcfe1500633a623ec4e65157ce59cbba97b8dc75694f238b9d980e93e579cc5b3c11a357b5158cbe89dd04a61397cd88524ba05daa6fbdf592ccb92821b05a5a09cd214a6c11162b1b0b250d1556573222dad8504cdf1dfa81b83b4456ec6ed7424597f602d6aab1641fa1e109d1e5ed934780e8f95a5ca829a4877cc3ec49e7bd5abb9ca0ff0af71ef2dd252824457b87f9d5e54dcc5443d8de0490d16517332458d89e25582890016f96e20bbbd6d81b9846d168eed1b6bffa6dabad8df76e144afddb8b5bde341ce94654ee214f7dca3d8ea5a5b925cb3749a3350f700d62380b187aa7e0279fe528d3d752bb741ba9e973d00720302f3ad688c7d538d1d968691578e29ea1bad131aaf34192c96e9753768f80b2ff98bbcb4123ff4bb8023aaf6d8e548c0d834a4d351da4ad560be24ab0a9bc72119a9bf2a8d971a7bf5ebd463410c6eaeb7398b8d563354b54ee99274701b0efcae547875679f7fc9acd9695f569f2d88c51849ad90aaa9c92a5884ccfd8b70c4e95c881d759fb98e0a003b1ad27fa629745a708507d6dddfa415c4e882e960e319960601751a6b4b3dbf9d5c735c72a32d711de64eadaa44becb4230fbb737f5363b3b2cee9b64036872356012cb66ad34b06663639d792681d5aca663e80a103529c6a23e2a229f5ea3bb33651a023c0d8503d3c24feddc4182830fccd14176088ef4c363b154e4d2bb4323469831489e33496294cabc1f764ee81beb08d1995ad6fea8e474bdd2ae988b2e5dfc2508340b9db7a7ad87549061e3cb90a167f894d1a11642920b20afdd87278191e680b07a5c84cab915974e042ec97af0c0ec8eb728f9dc062a0e5b9eaf8bd9aef74e29b0a8ebd6a179cbceadfde17e47a397cc96e46f25f6e2634a1a8d3b9a17b5e36b8560f6c8f508f2ef9eeafb5497300cc7f8a459675f4136f042f92a05a75ba8e9c885d51202888170173b04568d187a18914c246f6359938928a1e799e02f0580fac0c7c0271270c47db574362902cbab47f4ef90f0fe2f25c3aefcbe255abd1915edec356e67835397832bd854d8b191f664e1e721960ee3e4857974a6762f13ae7528366986ad0eaab18e53da543e6eb480782f5bb0932a1a6347dbbdb5d939cf4eb26bbbc1fe6ca38bb130f0aab1fd10dfac48fc5e165a16465840c33ed8e70000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x9b4f05", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0bd64342ca2f272d82c6a77786e961b5fb1983158a496af3aeffca02bd7fce6a", + "transactionPosition": 103 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff00000000000000000000000000000000172b21", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x4b6bf10", + "value": "0x0", + "input": "0x868e10c400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000008338808821a00172b211a000a651a811a045b2dc45820c84705285a78a4839d7dd9614c60acc16c72e827794f991eccaf716f0cff6ed95820fe634357a254919411f0fa829564b42326cc3ba9d7f50492d56d3a19b48234e85907808fd515f991b4f6a33de58875255e78be7d80628951a6f23f375280c3ec01e5aac19d252b2f8ddbe279ddb5758c96dea3b5be4a3f34581d5eba0e152cf8e649c5c272759b0b268aaaf09336e5ef9a9d0eea3b4d6e346f16370709411194f1274f03d6160a838e2d85e954e0070d434f05033a791cd1e0c149df1dab881b11423d43851c40be685faca80d7cc78959beb3975f8c49164ca96c11a2ede913c5da8cf168bd6407080e794bbac843e44f84c57b987103da83c6a9761c4dc00dbfa072b64ba1914e5588853cc3975f3463023dc27f04d6294ccf24b512c371066f3dc102db9c89172683d405c605e81488a03184c0b2996e963bc2297813598f82fa0ebdf4a13e95284e8807ffbb81573b8e73d495d18a53b9a25e39e8ccf36a50078101f9cbb2c68ad953c9e98e99dea30d8f65b821286709adcd4b99754a696e4f782e8715ac7458dda55863e46c87c10365a36bad7db606b42f49dad977252ba8fc2b615a9a68b6e4c5db36af1959788c9c01f6c42e30aec6b9618d475723feee7985d8d8a0119154f3a0cc0b44cfa94acb31e8d34a1eb9db44d389f5e6b840c80997fa17428581ce07cebe0e5a72a57d8b8df76fe7162c5fcd22771f6f39e7f09c420c298016be276d70715ad584b7e27545258c0c082f9df2a642e3f27b85db410aa8cd1e46b323b673d27f93c0aa1508e387d86a474565af8540c5912201af661ad35a7ccf0b7557d3047e6c467afce9b99ef937a026f17c63b6c56cfa2c075555e687959e1ac7d4ef164401b2dab427c4b8925eb4961db305123f932d15517b91d87218e46cad09696eb1b70f82d588c8d52d920ed13bf990f469bbe15e95c04402738842f4690e0d6963eaca9a214e9539f274424752c6855cd172f134fee67c397179ebabc8c67d87ac3862e9cb38b609aadfc476aa45acefeeaf1dde8e1d12cf3be18a24904f5261c36912844abbc6f986be2c730731556cb83ba4fb0a2e1c1f61836b805b0b404ef458aaee2ec5901f50c87c245772d68a062988be6c0178910957cd03a46d9d60cb3c6522a03e84fa2c4e752bffdf0faa82a1fe17374287456fedd11dfd4ea22163d1b3e0d4bd7c84ee73f63f8f6511ecd3297591ceb9bb59a6a52ca4d9316fa852ca949b496e8bc3ababaa8256ce13c413ff4c863fac0abb195ed523fc7ac26750864b33dcbfeb53febbd388ce3cf2cc541d9be71a8f0aa7ea56e806597ca6d08a4b763a68a1d8be498a1bec737872fe8ef18db169071932627a8f9192a498d2cb6a73ac23b3b88a0b1d3bfa271ce50abbe7d1b60cc1b9c0525b7d39daa328b854390c1538a69b6e73168143378d540cf1f11236bf4896da263ea751fe44d06031f0d65d52c735cfe0cbe82809adc1e09691e75a14cfb7c6e8f120544525c0ead12a3471bcfe8bf9aa3555c13c12a07ae2aae1ddf970b9cf714cfad51ff458228e051e00f2b46e1ef86870b6085b8e5820aaac1543cf1348d1fe21bb39dbe6352b52641688b2c2c666c8eba27394edcfe1500633a623ec4e65157ce59cbba97b8dc75694f238b9d980e93e579cc5b3c11a357b5158cbe89dd04a61397cd88524ba05daa6fbdf592ccb92821b05a5a09cd214a6c11162b1b0b250d1556573222dad8504cdf1dfa81b83b4456ec6ed7424597f602d6aab1641fa1e109d1e5ed934780e8f95a5ca829a4877cc3ec49e7bd5abb9ca0ff0af71ef2dd252824457b87f9d5e54dcc5443d8de0490d16517332458d89e25582890016f96e20bbbd6d81b9846d168eed1b6bffa6dabad8df76e144afddb8b5bde341ce94654ee214f7dca3d8ea5a5b925cb3749a3350f700d62380b187aa7e0279fe528d3d752bb741ba9e973d00720302f3ad688c7d538d1d968691578e29ea1bad131aaf34192c96e9753768f80b2ff98bbcb4123ff4bb8023aaf6d8e548c0d834a4d351da4ad560be24ab0a9bc72119a9bf2a8d971a7bf5ebd463410c6eaeb7398b8d563354b54ee99274701b0efcae547875679f7fc9acd9695f569f2d88c51849ad90aaa9c92a5884ccfd8b70c4e95c881d759fb98e0a003b1ad27fa629745a708507d6dddfa415c4e882e960e319960601751a6b4b3dbf9d5c735c72a32d711de64eadaa44becb4230fbb737f5363b3b2cee9b64036872356012cb66ad34b06663639d792681d5aca663e80a103529c6a23e2a229f5ea3bb33651a023c0d8503d3c24feddc4182830fccd14176088ef4c363b154e4d2bb4323469831489e33496294cabc1f764ee81beb08d1995ad6fea8e474bdd2ae988b2e5dfc2508340b9db7a7ad87549061e3cb90a167f894d1a11642920b20afdd87278191e680b07a5c84cab915974e042ec97af0c0ec8eb728f9dc062a0e5b9eaf8bd9aef74e29b0a8ebd6a179cbceadfde17e47a397cc96e46f25f6e2634a1a8d3b9a17b5e36b8560f6c8f508f2ef9eeafb5497300cc7f8a459675f4136f042f92a05a75ba8e9c885d51202888170173b04568d187a18914c246f6359938928a1e799e02f0580fac0c7c0271270c47db574362902cbab47f4ef90f0fe2f25c3aefcbe255abd1915edec356e67835397832bd854d8b191f664e1e721960ee3e4857974a6762f13ae7528366986ad0eaab18e53da543e6eb480782f5bb0932a1a6347dbbdb5d939cf4eb26bbbc1fe6ca38bb130f0aab1fd10dfac48fc5e165a16465840c33ed8e7d82a5829000182e20381e802209cefc42ae516244f0407001eccfd20d923218826471acf64a8a94d665ae51b2dd82a5828000181e20392202078d7f3d6cd9ba40272efa201f6dfb13dc5fd85d8cb3812bd5af6a23f0204c02e00000000000000000000000000" + }, + "result": { + "gasUsed": "0x32089f0", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0x0bd64342ca2f272d82c6a77786e961b5fb1983158a496af3aeffca02bd7fce6a", + "transactionPosition": 103 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002af885", + "to": "0xff000000000000000000000000000000002afa9a", + "gas": "0x6aa2041", + "value": "0x8abb7a55e7bee0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000072818187081a00019146d82a5829000182e20381e802202690c7f394488d63c61b511be885dd34412491e418f3022c5f2a70594a45da401a0037dcf0811a045adba51a004f7977d82a5828000181e2039220205fd60a468ccf211022611f407c8898cc32d838d4da91777a3eba380889390b3e0000000000000000000000000000" + }, + "result": { + "gasUsed": "0x4f77363", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xda7c7b60dc15d026b250310e1b2db33b40e00542386f18c560612ebae86ff301", + "transactionPosition": 104 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002afa9a", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x6976309", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xda7c7b60dc15d026b250310e1b2db33b40e00542386f18c560612ebae86ff301", + "transactionPosition": 104 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002afa9a", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x6869dc1", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xda7c7b60dc15d026b250310e1b2db33b40e00542386f18c560612ebae86ff301", + "transactionPosition": 104 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002afa9a", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x6748850", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f7977811a045adba50000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x487d76", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e2039220205fd60a468ccf211022611f407c8898cc32d838d4da91777a3eba380889390b3e000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xda7c7b60dc15d026b250310e1b2db33b40e00542386f18c560612ebae86ff301", + "transactionPosition": 104 + }, + { + "type": "call", + "subtraces": 3, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ade3c", + "to": "0xff000000000000000000000000000000002ade40", + "gas": "0x375dc86", + "value": "0x8abb7a55e7bee0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000007081818708192040d82a5829000182e20381e80220359da1ae33a951238ad1c46d5a5a8af8728e9f3dfb8c420a3c6989e1d451dc351a0037e08c811a045b324e1a004f084ed82a5828000181e203922020d07f551de5c94f8a5e6772d7a22d34450b43bfda443f5a059e35ede55ec0593100000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x268464a", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbf0919237db95ca6842af1209bd67f3c15587a92fb9f7f1343dd4e66c5828a06", + "transactionPosition": 105 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ade40", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x3631f77", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbf0919237db95ca6842af1209bd67f3c15587a92fb9f7f1343dd4e66c5828a06", + "transactionPosition": 105 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ade40", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x3525a2f", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbf0919237db95ca6842af1209bd67f3c15587a92fb9f7f1343dd4e66c5828a06", + "transactionPosition": 105 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 2 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ade40", + "to": "0xff00000000000000000000000000000000000005", + "gas": "0x34044be", + "value": "0x0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000f818183081a004f084e811a045b324e0000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x476423", + "output": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002e8181d82a5828000181e203922020d07f551de5c94f8a5e6772d7a22d34450b43bfda443f5a059e35ede55ec05931000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xbf0919237db95ca6842af1209bd67f3c15587a92fb9f7f1343dd4e66c5828a06", + "transactionPosition": 105 + }, + { + "type": "call", + "subtraces": 2, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce88b", + "to": "0xff000000000000000000000000000000002ce88f", + "gas": "0x1f6eaed", + "value": "0x8abb7a55e7bee0", + "input": "0x868e10c4000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000510000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000004081818708190491d82a5829000182e20381e80220e679eb60d986e754a677a0b5219976140fff0f798521f46d166a7e597b7c20211a0037e09f801a004f8a90f6" + }, + "result": { + "gasUsed": "0x17c8c83", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xb9208ab347ea30b568c4f9ab4fe310eeacfa107e9e5d970b38517334e51b2b2e", + "transactionPosition": 106 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 0 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce88f", + "to": "0xff00000000000000000000000000000000000002", + "gas": "0x1e45019", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0xfabb0", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000418282581a000286387ff107ef952f3612eb2e2a6606418e51c0f2cceeda5f57011731c2a04034dd33df402838a815a0b69ec1184b8c104a0001c0dfc71cd077c4b900000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xb9208ab347ea30b568c4f9ab4fe310eeacfa107e9e5d970b38517334e51b2b2e", + "transactionPosition": 106 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [ + 1 + ], + "action": { + "callType": "call", + "from": "0xff000000000000000000000000000000002ce88f", + "to": "0xff00000000000000000000000000000000000004", + "gas": "0x1d38ad1", + "value": "0x0", + "input": "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result": { + "gasUsed": "0x105fb2", + "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xb9208ab347ea30b568c4f9ab4fe310eeacfa107e9e5d970b38517334e51b2b2e", + "transactionPosition": 106 + }, + { + "type": "call", + "subtraces": 0, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0x4ecdc893beb09121e4f5cbba469d33f5ff618442", + "to": "0x8460766edc62b525fc1fa4d628fc79229dc73031", + "gas": "0x2aac9395", + "value": "0x0", + "input": "0x603119b1000000000000000000000000000000000000000000000000000000000000043d0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000002bc0000000000000000000000008908d75fa9b38aff2997fbb405ecea53ea376235000000000000000000000000066ce586cee61c33b6a7cb88b424f2597b03fdd200000000000000000000000054b19c8921b4b9b7bfc63a7c1267b8b361edcb37000000000000000000000000e5e0ff05a5cbc02fd0b386cfd626d3c537e49103000000000000000000000000ce53e6defbd1f0810bfcd691660ad311d0db24f7000000000000000000000000e6235c47b9aeb4309ad568945dfda530ba3ea4a6000000000000000000000000d93910f8dcbcbf07e86a0b3235d47f04b9b0a8a5000000000000000000000000280f1cee998b784d87f39047f5c757783a131bcc000000000000000000000000ea0fd4a321a9f1fc06e4d46a9e991ebe91302a35000000000000000000000000b0a258e65801e52ad9b709a06af61282194360790000000000000000000000008ad34724ea8c951cb0fefc1cb3b1184bc802f2ad000000000000000000000000d096dbb2a8eec6ae3f1baf9ce6e46ef4e1e0989800000000000000000000000018be4742d88664350e0333af9b493afa77b3b71c00000000000000000000000069eb6aab03510f6d90b206a5cc3e9f7b92349e390000000000000000000000008f3b852c5394ad9618c4fffc0db895e8570033740000000000000000000000004e89c37b3f7d9fdc01075f252c4c9c0e8e1db6e80000000000000000000000002379198a71e2a3cb94e78628a8ebaceee1c0c6480000000000000000000000007f2645e86dbf5fd6fcf613073bd795d0aa8546f5000000000000000000000000137536822037b7fb2e78eeb824af708e1aeeacb2000000000000000000000000aa60b7c819c4fe893e4f24a81ec91b4a22cb526c00000000000000000000000062ef4551567eada4d52c719f810bf27ba90136c800000000000000000000000043457a112c3065f05ac9fba437d184972aa12b03000000000000000000000000dad969443185072d2e1f657aed34c8d970b3bb370000000000000000000000004a64d6b9ef098ed25fcc47a537bb483e2963d0860000000000000000000000005700c1928f7e9cf7af2205de589bf12263db6530000000000000000000000000a31688aeca874dfcb51c26d4adef91bd18f09490000000000000000000000000a6de99edc8a11f761eaadd5b646d2cde410bfb290000000000000000000000000a4979051b9bfe1bc19be8f7793f7a7858292f3e0000000000000000000000001aba27436b6add4de90b81c813764c8a7ebf79210000000000000000000000009e36e3d36b8811be651a9bc7b6557dc76cf43d47000000000000000000000000d472d30a5f14cfe9e489e32b670c11f1b658d28a000000000000000000000000449121e6099b23e3064056106841ebcb5d9f82e30000000000000000000000009872ae4277420bb1cc0c543f63933c044b4aaccf00000000000000000000000098b622ceeac4f1b0118d1ad9dd8ffff809f2075e000000000000000000000000e5d713f13538289634ddfedbfa06fc9180d928960000000000000000000000008bf19b7bd21c6e1b1c562a5dc72b1222de467d9c000000000000000000000000e6badb95e881d2fbef09fbaf07fb5ba12dc6fa670000000000000000000000008dda03750aeaf62032b769ec0441268ebc716d9f000000000000000000000000a547820f60ce4e1133f69eafdd6ef8e2a0d2bb6200000000000000000000000041311a5cb9430a255e91306c0a2134a09c723f76000000000000000000000000d4ed562aefba750c3086b468f19e99848672c9160000000000000000000000005a45f28aeef32da96db5b6c435081a677bdfe938000000000000000000000000ccd5ac13bf889d8e87bc726403cd354fe5e0331b0000000000000000000000007bd4557c830d1b55877aefb30c7bd85dd833d938000000000000000000000000cb0a53f1e7f20a5b4d983ea043989f895daa5e07000000000000000000000000114238b63efd246c18dbabaf0992b5e6c60e5cb2000000000000000000000000be7217c9e2daa6b5401baa8e7e7a79cb4ea873fc0000000000000000000000000f9269499c33a7ba0e23082f058fc1d8c116ee640000000000000000000000006366606f86ae38cd817f3688d49efaf79909409c00000000000000000000000060697cc054ac2e09b82952783b943f98fe75054a0000000000000000000000005d48ebd30cb6f1b3c901764df6472266cebc44eb000000000000000000000000fc253c6c81a5f2b74b804914d53f823b1548f81300000000000000000000000025cc895a5a051cae6874ce6c8dde6637317605fc0000000000000000000000006cf7a2c61da635eca30c5935a400f078a59254a60000000000000000000000003bdb257836e15857a610aac284af3502fc2aca200000000000000000000000009e665e20d2a11a71c9c3a37891b9a0a92e0f01ad0000000000000000000000006e5afb67b5978b1f7e56358577c6bf93789fe572000000000000000000000000f64ec0c50cefa072ff9147dc5a7dc4d3d8a0e6a8000000000000000000000000671f941215ff9db2f38d49cf6836ab0e79afc2c0000000000000000000000000faac9ecb2638acadd9698ba42591a1ec743bddae0000000000000000000000001fe73dd41b686b586fb8f9df8b3660812f608bd3000000000000000000000000d7ce18c2d099c482af073156db2882e8db851ade000000000000000000000000a319ea952a8dba1442c82a4f4f600d3a45dc0e18000000000000000000000000320d66c180785e5c16eb2ad89edca7ca5eb0b0c1000000000000000000000000b160e2fb32b315f0680d0e24a802a4a4ce9598c9000000000000000000000000fc96efc7e05aaf90c3fd23a6bc9d8d4c37b915ef0000000000000000000000007648ab9829b0b50bb73737707c5326952cd7cd23000000000000000000000000aeec7e0100c6f7d0a21666dddfe135cb4fe82306000000000000000000000000162161f0688b0056fdee01153b49f72ed1bffa6c0000000000000000000000007c3847a5876f80296b8504466d28cb92d44957ce00000000000000000000000010f376cd97c8ac7971e256f423fd63be80ca02be0000000000000000000000006899f9339471fbcebf0b78d87404a4bf7f741d38000000000000000000000000d49654bc9eaea08bb21f95e6f1839e92b3fb3352000000000000000000000000dd228dd2a81ec9f32fa17eb82ad0426c644d74960000000000000000000000009b88d5a03487335a05071e1291f416fb9641b8a600000000000000000000000081f6e5e5a83af4a6a153554a76a6948a7ba89664000000000000000000000000d5280e81f1116febd726a8b77501db93c65b783b000000000000000000000000a786214c5d2a4d5ec024fa686134dd73251dcd8f00000000000000000000000078fd38673ea90fa7f68c3c89004b373f7473e9f6000000000000000000000000859bc6c9a987a086aad26c0a0f910ce5cf5daa710000000000000000000000009a1f203bb3280fece69bada8a1ff1c0ec8d27cf4000000000000000000000000f68d6242c590bcb5298ff28f3968db1ae0841a020000000000000000000000007935c0f5c4231ebff41ce526a50e039ea77ab120000000000000000000000000d12b882f7cd12919e9feba6bf62d43aae942197c000000000000000000000000239886f67d8ea66d918f36c56d36115cc497f3d0000000000000000000000000e7bd0aed16097e245afa588dc2bb03c0fbc6786e0000000000000000000000008d0c1b15756be79279c30d13a9590fc1bc8a88b7000000000000000000000000369eab5c688d93d4d5126d848dd9a6d979df51a2000000000000000000000000e919edecc18a570c521ca0964a06464bb4aad2430000000000000000000000002f6a1b9c81f67094d11242a0c33ce10637ee8f30000000000000000000000000b53f321c0fe1e5e60929071e250191425983341e000000000000000000000000813df108a698b94adddcd11783999f649192d7aa000000000000000000000000fcd36d472645cdef280d4b7c3993ec2a90d01cfa0000000000000000000000001885724fd53172e5d862455949f9fde38c80c9d2000000000000000000000000a15bbd0012dc46ef6d222346e6bc4d55010753cc00000000000000000000000012341357e848b29e926ab86f3e5acfd6af451bfd0000000000000000000000004873ca40c9dcbb5fcdb6e5aeef5375dffd8e539300000000000000000000000048ede89338c4ddbbefd4743b3cde9e5672e6b6d1000000000000000000000000d155e64d9bb676006b64998c05508324f4f9f9710000000000000000000000006b0931570e75e89bc9f5ead3b2863eacc0e3172e00000000000000000000000091c291f1b8a414a5049ab689f85195822a377e23000000000000000000000000cc072c2a5bdab10fed049446ce12377f9e033e0d000000000000000000000000198a584e24d3ada3fcbdec1d7b0038833314fd9500000000000000000000000008ffeef28946ef3dba23372d7898be39f16a2ba2000000000000000000000000caea702803dfde8941f87919ce233596800a043b0000000000000000000000006166dc613bc40506042daa7396932adae383bd3e000000000000000000000000a494748369b21af5c869fcf71fe2971c64d21256000000000000000000000000cf9e38d0d01e4eb0f26008c4f67847e702f172ce000000000000000000000000390a9d994552822c16b84aca0117c9ff3dd8a2fe0000000000000000000000008e3fab1bae9d560c14529e0843427f69c2c328b9000000000000000000000000ede338b3c6ba9103a9c903d94a62a9a95edfa93900000000000000000000000088c0d01274f0765ca12f9b594ac1bdb70db09a3a0000000000000000000000000b573e104878da66d49cf33f9bc0e522512438db00000000000000000000000052865c5a84d51760824820cafb5e266554efd5890000000000000000000000006e6b41de78051f7c9ab381bd2e0256ecbc8272a7000000000000000000000000599b04bb8266f01b0fdbf547e253c37210a149920000000000000000000000005c7a4e019866bc832c8eb38a1cb59690683cf2310000000000000000000000004d037ad3b8b55b0ec8b44b2432f9743bf3c3a88c0000000000000000000000003b1732a79f372bd5700ae1eaa3d6daba833f0f76000000000000000000000000118bda236a0532b4a965fc04eb0c8c1c7a0c109300000000000000000000000081011ce12f603d8777abfe4fb4019eb4961f6160000000000000000000000000ec25125cd1bb1d4a8869bc987d305e3f4838a56e0000000000000000000000006294b6595081279238e5d80fd067b874271babc30000000000000000000000002eea10652c5fad91bcc827dd9020cd76e164456d00000000000000000000000011530b782cab259b0a36d62cf6463549b733d4f50000000000000000000000004bdc0e598d89a97072706b31a893e6d73d8c4859000000000000000000000000405b791237c5038f302566f2ed0be69ea8cbeda800000000000000000000000029422f22e47b48594b5c51033035f7213e43f362000000000000000000000000ecdefbf081c278da64dd3fd1937e1a0268d760870000000000000000000000005b0cbe0273d74f22018cf1b37aeb9ec8e3a1d4d8000000000000000000000000ab1be877bf15d074854b89536ced1d9088b55d3c000000000000000000000000988f66a135e579b5cf873d86496215f37401c4620000000000000000000000008699232f37afe708c4388acd0399ddf915c2f110000000000000000000000000a9912b8affd2b4eaf15067218b948a29a54ed6df000000000000000000000000af0aecf4a4d7f87a367b8ddb69e306e5cb7077710000000000000000000000002698ab33b993eb72ab833cb6bbb648a349bda2d70000000000000000000000005cbe0f8ca58fa82692e902ff0eedc79aa25f6d3e00000000000000000000000038169754af0f75f1de60cb768d3af55c588f52960000000000000000000000009e596e717f205b1a6582d86a16b773516df62ad9000000000000000000000000c60927d5794df3b57926a283e5a9bb211d27ed16000000000000000000000000eec6bba315c715931cebaa87d80336b468cbfb0200000000000000000000000053485d58437895522058b3869a6202498d2513230000000000000000000000000e83a2d60778fac08aeb930f3c320801fe2ccca200000000000000000000000042728c70b8abf85a8108ccba50ead645b8105440000000000000000000000000f2abf91e6d6b44e2a213350aa95ae3feb9046f840000000000000000000000007ccb0c9d2fd0bb862265df11b4425f74b77a44120000000000000000000000006a5d831a5171344ad921a4402fad0b961e76e17d00000000000000000000000056e9194e18db791f5b25937c6e570f7ed7b15358000000000000000000000000c841203d9104b3d3f58d5dbabb93f9411c0eff54000000000000000000000000b462cdc9777759a70f1f3377744aac293f40ac58000000000000000000000000279816532ec96b16eda9bb36aed3ca911c7db29e0000000000000000000000001fc416d6ce0af7aa72a0e3d2d622cfca73581bdb000000000000000000000000e393e4d2302001a928572eed995e5c61b4ee6350000000000000000000000000ef2935317885ff5750b58caf90c2addafbda7efb000000000000000000000000254934859cb06d70e72ee28e1852a0f1e321430d0000000000000000000000004f73b09126c5bbf0ee674ae7ca3bbc21b531e0ef0000000000000000000000000a7e2bdf61df32a1907a3d9325c065a3d76ede190000000000000000000000006eefe64ddb2cf3d59e63dcb065154de1512db2c800000000000000000000000055b51c17fc82818b32c0ae1464a3fcb0534e4d5900000000000000000000000037b6c31b4f5f17ce9e67c2144e908185db4d7a040000000000000000000000006213a491ea8a3c34cdea62844b4b2e58270ef6c4000000000000000000000000db2b72ee0c7adf4f6fe85ccde475c67baa29f4d400000000000000000000000086eee8b8d175341ebe65bc7b579fd90be72d59100000000000000000000000000e9bdbd393697e495e3eba1bbdd0b8e3afba0f98000000000000000000000000dd4bed194cf26e3ff836b5c6735a21a1d4fa8aef000000000000000000000000d9434d0aef7f702c558108e8ded78ca4f18ed93e000000000000000000000000ad8826c9b289d4f60e03faa004aaea160353f09900000000000000000000000089b124570529d9f8c89d85b32362a430626dad90000000000000000000000000d53492c49f4fac94027597fa35bc0b7bf42858130000000000000000000000003109643e03cc8d0af5577819854e37f1c3aa944400000000000000000000000012f760c4c3ddab0f7594b76edd807be229dc488500000000000000000000000046a8b144591746de657b136ad79c5f274ef61c86000000000000000000000000c99c12c10832c7762eac23d89cedf48249537ccf0000000000000000000000000573371f08f8e2b70dca981b255f19389f02a0ef0000000000000000000000001254d674dfe65f3f46d8c5d208df0005762dd1f7000000000000000000000000dfb86b184f801327540b19ee64f0cfd17faf96350000000000000000000000000f4fe5a26349044865cb55fcbed6f42522d89be8000000000000000000000000654bd531b78d52ec2bb8b9ae3e4a6592786e1b6f000000000000000000000000a84f80e1d5f1bdf6a01523d4b6f4549b5b0511ae00000000000000000000000011466ded9018d521dd4014605c337a3d3bad09b4000000000000000000000000093d1543dcf93722e1b37d13ce2c2c7b32531b980000000000000000000000007b7be2e99b0bfc47c30519cd2a61bf7cea479f79000000000000000000000000fb559565d4a10c730c0678ef58b739ae7adb75b2000000000000000000000000c0ac795873db5a707e64c2142c663262e2cd8f460000000000000000000000006b99e9cb2c3424b370e42383938f06249222e2c200000000000000000000000039287b94404df817797eb887cb3a387e2a27928f00000000000000000000000060ed857098ed6047bb468a16aaf25dcf273c1c6c0000000000000000000000007dd0923c0cb4d5b331eba921f05af043b796e6b400000000000000000000000075b35cd84d485c768297f8f9afe4b16feadc9104000000000000000000000000015053de878fcefb7ad5db719d6bdde6412e0321000000000000000000000000a3e56c6df3bfadbfcd65d2c7c1046284ce6d6067000000000000000000000000dd0a1890b0f3213102a9f858ccc798b3aec6b94b00000000000000000000000076301ef56a524fe83b891dccefb31178790de3520000000000000000000000007fe88717c62f0ca72ee22e8641bf92c0894638d1000000000000000000000000bcb4ea5da37b9893ab58803306e7150cb83e9752000000000000000000000000fff2abee60d164a8cbfb244d10a2b18dce0713b2000000000000000000000000cea1f873d995ec94b373eb69db2af632311182250000000000000000000000006ad8fd1a1d1916e7aa6f3cabd9c3f478dfef649b00000000000000000000000039d9293694f2b3e9302019b51b4e491429aedcd900000000000000000000000041f4622644bc6cab32938a5bc27f4eafc873efd7000000000000000000000000364dfbbf3861d39aca8fcd6b57123611e98dffca00000000000000000000000092da7004d5cf237bdf4658c9ac19caae13676b22000000000000000000000000e847268ca3b533b00338bf53615fb4c38f70492400000000000000000000000007c58779155a925ed1f5ba2d610d6b02462283cc000000000000000000000000de7c9084a48266320bbd883b5e3e1fbc8ec41e28000000000000000000000000e28ae523d8bddf57e1ecc370cd87d8c16443cbbb000000000000000000000000edd0eb4f0e8bfb28c1ac997c8d1a05c3571eacec0000000000000000000000000779ab62a4587531a1508f1364c0176ecce0b8980000000000000000000000001eabf73ab1c362c2ee70a47b1e0cc8e45f301294000000000000000000000000477cf615451cda2857a049f693e7614f45e548180000000000000000000000009c93175043bf08bd37f8c83b9094602470ca8bfe00000000000000000000000061ce230157c791e12d021001621e11d9f6a4f552000000000000000000000000452283b7c46bba04305654502b0a5d0553680fbe0000000000000000000000006afab857230275fcad0c75114a2d2f7fc154e6c7000000000000000000000000c34e83eabe5e64e903a9aed1b76a8942a286730f00000000000000000000000007221f9fab99c7298e20f182771222dfe118109100000000000000000000000039e41a1c062cd4fbbfa1d7aefe828384daca508c000000000000000000000000622c611ede07222a30f070bf0865437515c21bf9000000000000000000000000db18785ea18f2d4936be51850f368cbf6a6b505700000000000000000000000057177e72844c7633cb14b2ad702b3f1cb7b8924e00000000000000000000000001447dec147917844cea959b914fdc48d16c652100000000000000000000000095eddce628a95cdcf037f84977786b4daa993cd80000000000000000000000002ab19c7edf2e73e58bf987253561e2d1fef34308000000000000000000000000cc313f29d7b55da624cf86728fc1558f353582500000000000000000000000002e061715d5db6b03003a6d5d3745854a8908b383000000000000000000000000705e48b7acc67dd54d90ceb3aba33d4953352e9f0000000000000000000000001bba8da0a4550bb23f0c94f660d8794fa03d27620000000000000000000000004afdc8c6c9a8b3b8661c242c0145c491bc2a283a000000000000000000000000d6850a4a2d9ab65ae27563c543eebf20749341e70000000000000000000000004c68e6e61f7ec16cf79268bfc678e4a97b487f3000000000000000000000000058125994f6f0eabcb9bf80632726ce9610fceac3000000000000000000000000a2e3e1bf252b9eb1437ea2b2f621ae88f5f028080000000000000000000000005fc2c4b5a8dad8794947d134bf0de97ecb220a170000000000000000000000002ce5644f2e470b664e0b87dea744e9ec5e448180000000000000000000000000902aeb78ffaf5e5002ac1646da4c976e3cd20edb0000000000000000000000009f4ed99a67143d9b6f33e7860897d5319b380867000000000000000000000000906c38909af9cd416973553105553b2d1c0b1b670000000000000000000000004a05a2a6083924d195e2d0d656fcd60cf8061d0b0000000000000000000000006035cdb4e3fb3c99b5409da5fe9faaf041bb3c68000000000000000000000000dd3f1c9ea6c073174941de8c10bb6c977e5ccf67000000000000000000000000bffd404ab45201de5e73b8a45c37d91fcf556d340000000000000000000000000d75b9ec54fd3ea0231d7a5f566ece388075306000000000000000000000000069bc8512208d70cde4773ea1796061d222ed4fc7000000000000000000000000fe58475f3d6bece4d8d1493574be8b2a5df72dc5000000000000000000000000b67cee1374515722284e03e432477dda8aa3de130000000000000000000000007e9957075c46708b11bd5cbfbd1b0fa4c9365e17000000000000000000000000864150f2da1e98a6cd6bea45d93630d26a3c9e95000000000000000000000000c60554f08a76a6fbbfb1a0649d461d442e9882df000000000000000000000000a00b4f0670d28ce92b9d317f0c69278646348d9e000000000000000000000000c6a33c5cf611816110e1acc59f6f6cc551119bc6000000000000000000000000cd489fdd0ef4d501c8bc4077b5dd04709077f0b2000000000000000000000000a0e855a4618c4c65985be4850dc8cd6b769c1855000000000000000000000000b7976f651df2ec32e7f061b6db02c3f329a0c8ce0000000000000000000000005f8a6eaed330fc1731762364a11e375e28a7aa820000000000000000000000006a7c6d615feec103835779ae1e61dc2080ab0028000000000000000000000000abb29ea8981e83c989fe7cb608abc4b1a19cb18100000000000000000000000073a2f14385c8d7b7c739a43bf91019893920f953000000000000000000000000b9f5726ae8ae704d1ae39e6635913d1db654446c000000000000000000000000079476d18a486f67cd177462ef9cd0a24fa3d783000000000000000000000000098fe625f917e1d8ae190c1b9cdb9e7d854a94040000000000000000000000006f214b804485cbeb9ac3458019b6312ad734ca0600000000000000000000000047e58e865eaa11214e3560b74a3565c51ee19c3b000000000000000000000000a27d25421f4a0caa73c0482b2888ceafea15896d0000000000000000000000009be0c14dd4d17092e66643b384f101ae41c366d6000000000000000000000000650bd8722b4f6d1e291411788421e21b9682dd43000000000000000000000000b119de3bdf476b726261fddb34f574a19111f1ce000000000000000000000000aff6acef00e48495324920c61dbe226e362aea5d0000000000000000000000000e880fa4f87bd1231349966576ea34999083ab0c00000000000000000000000082da0a99f731bd3cede407a5082dd5ad38a0fa660000000000000000000000008dd09ea0c1486bbce042ab63bd1f00575ec57c10000000000000000000000000ae9fd3be2ad3ab993e8b12f53acb3370d2cf85c30000000000000000000000003de5e8bfa47dc9f7067543196dbd51c8d3a03cda000000000000000000000000f77c1491ea35053886c32542779add9b1bd8ff4a000000000000000000000000d1cc923e28d5cb89e60752f6640f923bc4f8450a000000000000000000000000f606c34a4f67344c1aa9008c77d3d1097005d42c000000000000000000000000d68aea6028172cfd417bbffb1065e2baf832bf5a0000000000000000000000006e763631e535de674135c4f64e78110cd3fc891a000000000000000000000000848823f7dafe1bb36b7e64d188488b70aa4f6c6b0000000000000000000000007dcb17cfc40f9236b08c08adb262b4099a3477e3000000000000000000000000c987478064f7a6339f7a0c63e11053b85b10c88d0000000000000000000000002c69f0b0b32e997ca9fb1adb9b122383c01bc96600000000000000000000000079a87ba6fac6983213b6c1421593a7d64e2fa0be000000000000000000000000497a1f6d9b13905db63f2b9f16fb7cd22aea406a0000000000000000000000007f8139c8269c917085537ad68e3600ddd9eb89c90000000000000000000000007d094037d0f0d5a5ebf67258cfe783ecf57a74040000000000000000000000008ad198abd937666cb3376a20ab6ca257f96f4e2d000000000000000000000000c9549d267d39e494770297a589a5a0bf3171d1690000000000000000000000005d6edea6017e54154e5aed9841e4623bc8877c04000000000000000000000000008880fefb10a5999473943edae788a6d54e4bc900000000000000000000000037668b5e0fc9bf6a45e367d6030b6609bbc32b61000000000000000000000000ae6ca04ed7eebdd5dd256e595072c54c304149d900000000000000000000000008d347e43e361c42a31daeb18ef2cc0d190d6bf10000000000000000000000007f50d5d38ea40b4e331d32aa85dd74bf7ad7e5110000000000000000000000004846c8dde5747a69960ed0dd8538c1b7ba393e940000000000000000000000000df2b635075fa358eed7c126baff1c9b862d3bbb0000000000000000000000001deb1e3a9a33e99886c7b3a91e34f2ec0d9b52c9000000000000000000000000ae4f25c994b518b4a0fd37f666e6f8f77daf16d20000000000000000000000000c2c5314dcd0a7bc928ab028c8ab384f9ac26ab7000000000000000000000000e1d0cfb183ca26466b81bdfcfb1d6775e3f4bb370000000000000000000000005b3c938e6f0303047b57c7d8c13e37783f795ba20000000000000000000000007090e22b5f2d4215a635beb85441b6128d011c76000000000000000000000000a1a93a8431d224f5f99a741d26433d90a4cd069a000000000000000000000000739dee60308f5f7a05be524b0785b36c95153402000000000000000000000000745ca64c4b7519399505ad7ea9ffce90f9cf3d3300000000000000000000000098c9c491b129a73fbc67fcc6afe4b32803bb34ba00000000000000000000000072d721c88690147421092956b0e58bab0bdc3034000000000000000000000000584c6e21412c7a6567c357571204acc2bc57430400000000000000000000000082735888d1c4043bfa804ffff5eda0b0e52bb4b5000000000000000000000000f9b84befdfcd0882aeeaea24f850430ef8666aac000000000000000000000000eb75d14fbdf23f1e79033e99706de359e4316dab00000000000000000000000035c443afd895ffb2b769a3416191a9ea500f946a00000000000000000000000081cd0d8c36d9f43073a6beea7bc2a143ec66885d0000000000000000000000002fe1bb61cc67cf64cce87fbf835b836f6a3c04e0000000000000000000000000c228d0a9137cc5e35bb9770926d138a19d1f73470000000000000000000000007816aae5c3a50999deb2c4467aaa4a1fe34507e4000000000000000000000000c6aac9efe2f7d96bbc1bcfed1612318daf302c69000000000000000000000000226bf460f545730b0cda9cf96c0393a55cf714fc00000000000000000000000017d0827fd96765a7af1018cd93006ccf1fabc5c60000000000000000000000005fd9239392ed3759eb48691b58ca41cea79b831100000000000000000000000011c337ecb620bcbaa3b8b9aaeaf397bebf93eb8e000000000000000000000000f37c8510bf7e21c556e13b625b32e1ab3b790d6700000000000000000000000051761896987eefacf1be63e919d26cbaaff33a300000000000000000000000008d0245ffddb94e6b0012e7a7c44728140aeb1503000000000000000000000000ccf9a84d7eb7eb332377a4ade4bc18448ac47eae000000000000000000000000400acd777f435768d5714d59544dc212cc750b47000000000000000000000000ab67ab90b3e8d8d1d31cde0d69097006819122d20000000000000000000000006261c7f5628c552e245db265b45d878a416b059400000000000000000000000002b7439d09fb3cb7e4c9d4124c06feff55c09a1b0000000000000000000000005cf6052840ecea31369e7bf426145ac2aa59cab6000000000000000000000000e14e36961222c06258eb01e9cd870905d24453e9000000000000000000000000ae1a546aa603395dd8ad9cbddd1c10ad36db904a000000000000000000000000c1b369f561a27830559bbc770ba4fbf96c255eca00000000000000000000000024b7c11f7e06ec108f2b924fc90a49120431fd0b00000000000000000000000071f5a573e4e06e590757326e6a63b02706d319bc000000000000000000000000c4df171fa405df3fdabb136143055cc04ebe97140000000000000000000000002676198d802660bb4121db3afcf1e11540f5d6f9000000000000000000000000244b8ca6996c29bef8ff7d83c6faf4efe9387d6400000000000000000000000094c15b3e5516ec5bb616d17a5833cab29c7d63f20000000000000000000000004b81de6df8bad151f2593c3ffa97c93879e8d9ef000000000000000000000000d32ca710b42fbd87f835b9b595dd1538b605f2f8000000000000000000000000edf1f554a63311572f8683dcc8a09b872d926928000000000000000000000000179f95c078bfb5cc303f54d08da04696833077f2000000000000000000000000d819e937426a84be689a63dcd4adfb2941ff22100000000000000000000000008c54c93ee4b0e630513f195368b615b9d6e236c1000000000000000000000000a758969c692085ef88173c392f2954270b9827bd0000000000000000000000007f1973e5f493ade52b98575bb513c2ea8bc52d93000000000000000000000000522695d7e879c5650759412a73745c7dd76cbaa7000000000000000000000000dde758ca495cbddce69c6fda4acdc855af11d4a30000000000000000000000003d7ff7c5c8bdcc4b0d3e746e20e44024762546ba00000000000000000000000054c58a2db5b2a58dbba43dd1e939daec6445872f00000000000000000000000058331c199e67a9e991e2061cbff70bcb15d65a49000000000000000000000000fc7ce4b74e8b1b5835caee6d34a6072ccfb36d8d0000000000000000000000004bb9a03990ffacea5d23e722091bc5d2718d1cbd000000000000000000000000f46b13d14a9d74ace7557fa069c7d63733de5fc5000000000000000000000000d2bac8ebb2f6f5a92cd13c0b5477f916d0d29bef000000000000000000000000bd1a4ed3b262ac6432602b7a18ca65ca29a364700000000000000000000000009e9e83275d92d3dca640b2f8c14d08e389a75f9d00000000000000000000000014cb0351377e3b206e63dbc435d3f185b958e0dd000000000000000000000000ec820902c78106daf869d2bd1a5ddb7419ef40cf0000000000000000000000000fa9dfd93a1a51c8eb7dcf025730d1ac02f83fa60000000000000000000000001cf0088c14a5a4571a41507736815b00c5e532980000000000000000000000006c1d498debc51fb25b6d06492cfb057e74ee6be00000000000000000000000009d7ec8b52818eeebb6b893b2f09d781c024e0425000000000000000000000000dfb671a4f29e9a7747e5a9bfdede8f9fb81982ce000000000000000000000000d865cab8d312053cb5dc3a8e2b47450a1f9f25ce0000000000000000000000005c6ef10648213c486570c17c76cf0922f9edca4700000000000000000000000088d3a16433ae3833adbc0ac1d3fb30e69cd3bdf7000000000000000000000000615a1d028784a273ab0f465795d793398b3887c600000000000000000000000091f6add8c6992bc3efadd528c20ace3ea9e2218e0000000000000000000000007f3f80ca889de690aece0db872e4c39f52ee0f170000000000000000000000001481f4f9cadbadf25f873631afb6ae22d25503a4000000000000000000000000d5c0356c800e7191a5af4cac2b7ae9c44f07b049000000000000000000000000d778ea38e4ccf3511a0f59c9806f37c7bda92cff000000000000000000000000ced5a895b7970f020819eddde9b0616d2ae053d700000000000000000000000034470cfaf0fbccb5935a6bed3baa686989b04eed000000000000000000000000c936019d1847acc03d01362ff05b19c521be154d00000000000000000000000016157022e231bd391cbe55711d492faa7021b805000000000000000000000000e91f81b0057862d8c5ca642a05db00ddfd6cbd02000000000000000000000000942f6da946cf77fb53170e8e416e595f5e3eb953000000000000000000000000fbc8688736727304d06252134cbba29ba95d1a6d0000000000000000000000006580ddac5674726ac60dba4470024615037df0350000000000000000000000002f7e4cf14adf900672408a7d21070ac34363df0d00000000000000000000000074f6e058315fba3ebf55d33cc6505ab5ab28cc4f000000000000000000000000f21eaeedfb2d605ba091bd08e7c30b23450dea2d000000000000000000000000642df68305263e02627260098c6e023ae29bdc75000000000000000000000000c3037ef8ec35c709f90f2981f322416c8d0fb8830000000000000000000000000d5a5df0f56b7c0c2d77e15a4b066034c30ba6690000000000000000000000008ce354651788679cb1385171412443fd1b51c16f000000000000000000000000d89f429469eaf8e36014149f748ada5404c584e300000000000000000000000019dee827123e28196c6785ab13a8d15cb6721ada00000000000000000000000009cc1b107188bd4b2db85ff1a9731ffdf2735cad0000000000000000000000008d4535ef821c773a905c7bb5aabad9865a4fe9a20000000000000000000000004399555e2fe753f7ae3ca5bf1a6735219733303a00000000000000000000000027af5c6b3b5606ad2d495c90738ed36c2acbb26b0000000000000000000000008e21dc17387e56705289d8fde6aedd71570b0598000000000000000000000000b884787b220a857af3f7d87da295cef35a60ab900000000000000000000000005a33449e1068622d4b9189a10e6b06f282668ed4000000000000000000000000dc69eec0c0c5135d11375bea03b76ca939566a06000000000000000000000000cb3eb3331d3a3021dccc0695f7f5bb356b38ccc80000000000000000000000000a489b5fae86079803fd840dec2cd6a8c9fcbad20000000000000000000000000af8d0491d94389ea61e5ef79f3924581e2e82c1000000000000000000000000db2fa4f76fd1cbb08b52e57387f2c73c8cc067f9000000000000000000000000a152e9b26df4eddd79115ef846bc74a1f2f853c70000000000000000000000003050eb14394f11e8cb74a1af6db83db05fabb12b000000000000000000000000264aa32dad1d8f0388b787eef55e8d5dff7d9d1a000000000000000000000000e599f271f0094c3fd5532648b7436eb68d4025ac00000000000000000000000033b9c695c88df21067855fcbcad5f7ee20d0b9820000000000000000000000008e27a7d508f292d709d7099662f42492bc8e599c0000000000000000000000006b27994021433c75b5115feca4fc31c9e1c3df21000000000000000000000000d299fa49a47faae8f7d1b69f0fb2588687eb4e5200000000000000000000000065efa2d869cffbe5558e4bb789041e32e75f785a00000000000000000000000044bfb7765841a406674f42cadcbf2bbfe69117bc000000000000000000000000c11c272f7c5f127dd911dcc3bfbe62872df77d8d000000000000000000000000f74662dc19c3d6480627e7e7542d28b30a3a1531000000000000000000000000743b0c678f819573d421c8d8f017281d11bf5056000000000000000000000000b8d86887bb379db33a17c11a7ff9cffb16e35131000000000000000000000000c6e678f796591bb22b58571f5db16fcf5a8bff3c000000000000000000000000ac531284c469b20ad3996dc04ceacbff3b4ca2f600000000000000000000000049cc90f95fb3795ada93bf86eb7a55ed93f347270000000000000000000000002cb98f1b05213be609d4f8c9099e5265760c2ab3000000000000000000000000966eb41f9215f74a76f15e101138eff62777231200000000000000000000000077daa2a89281c0936420f7ede2f0e68233501a54000000000000000000000000e05546e3eff21f2fb0fbec055ef6cba23f77304500000000000000000000000052b9d6664da609082e51f0e7a45a7a945de629710000000000000000000000009a6bed0f7f31ea12075593e8c3f7b3d7c16c6f8a0000000000000000000000005fd468903fad6436b61265101e10022292b0f3c0000000000000000000000000b07568a6853d97020a69f48c122c84d2aec76d92000000000000000000000000d0585568d1bfb1f626cf335c6c3e6cdcd84754f700000000000000000000000065cb1900ed9f1f7de66776ec75bd12bf7ab0163b0000000000000000000000008f7c8aa90ebdc51484a16e036b0797667c861912000000000000000000000000f6a6c660c0858007af06dff44050106e49227f57000000000000000000000000cb4e589c58b636291b73f36428a2901f9a901ced00000000000000000000000012f6c398833cbea0b56434d814ac107fba0c9bd8000000000000000000000000eccfc303f108e382b192cc9aab70e30a7aa7810f0000000000000000000000007be2d5150d7d7efdd0ffa8a957dec8895f8d1abb0000000000000000000000007c172e273d6feb01c041211140f39ea8be27b72500000000000000000000000054514197caaec3db94451f432c5092c3783763870000000000000000000000002516ffe75fdc84bf027ee14352839acf63cb041f000000000000000000000000c6508db99f4db213dd71c34784ec0505383789ce000000000000000000000000f8dfa50903d65fcc03da6348bf8fa88483965b160000000000000000000000002499a068f468f8896f2b7021cb04092c2476f56300000000000000000000000061ec0aef14f58a89df9749520a171eee30b4feb6000000000000000000000000b57935e3997173d8d2d471fbf0c5c8f9ae02b0c90000000000000000000000005e64578c4cf7abccb32a264cdaf0c3261975d94e000000000000000000000000c4bb855893df113f1f3616c942e683a440df52cc000000000000000000000000daf5025ec0844c9dadad3b8b3c4cad41b97d6596000000000000000000000000160339dc12dce4971ee95c545931c12bbf6c5287000000000000000000000000b59a8042ceb28de94b49601979d7910172156f03000000000000000000000000e8889b3afa1d2a37996cf3ac56c1e633b4f50dac0000000000000000000000004f001b31c83f6891c9ddd41dcfccee9986d2460c000000000000000000000000449362efaf626063228251f44f61770ed316ad5500000000000000000000000005da93afc48b874ccf93d3646b6a49cd4d0b321100000000000000000000000088b3adba67e1e3b16b11483fc630bb3f81c73b6d0000000000000000000000004d502f02b8da038aed30ab1b8524b6320c84ba1b000000000000000000000000c8d69f95a6e93981bb772f63cf4d61d7a905f9a1000000000000000000000000d4ec18973cf3af5779886907124e55085e91d6420000000000000000000000000de5344d8be36b90e7ca73301102981d3b2fda250000000000000000000000001174c8d3fe1ce6fa955c228760300e91500e4da1000000000000000000000000eead4f379456506a2451852ef1ddf1d8c563cbd7000000000000000000000000707cea598b987ec33371490b4675f3af9eb2b1dd0000000000000000000000000300248c6045adae61585f2e454da9c77b25b46c000000000000000000000000d1319e17a1bf454afdf55335a0dcceb6d72607f9000000000000000000000000f12d4e9585d3f11c9fdecd660a00717bd7e296de000000000000000000000000ef38a797c135876314189704f07725a785d062b8000000000000000000000000a5ded0412981543480a26de10d9575e680dcd7ae0000000000000000000000000be8c925e6d7bbe0ab308e77933bb04e05571082000000000000000000000000e007ee43f5effe8107efc3c5a80bdedcb5959c380000000000000000000000003f108f6b29f941943bd3c187b7dcc97337f0e28300000000000000000000000088efbadfe921329dfc18ae698a56e1917c62764700000000000000000000000032c14c8b0df2ade0d4deaefa67340bbe4b312a910000000000000000000000007058084b8ed1d2934b260b546aecea79ff3ddec5000000000000000000000000aadeaeae853d88b675cd126a02407a55190147980000000000000000000000003b5a09804421c0cc73ade78219e8009f611633c00000000000000000000000002b3241338e81947959246847e199ef164bebfcbf0000000000000000000000007226122e864d80295771965d3df6760ed8dc894c0000000000000000000000004153b278035d95cde210ee624444aaffdba85a4e000000000000000000000000a2ee16c8d256ff2b3390eb20e6481ae43d7a8081000000000000000000000000b8a41cfb33ec38012c542d28a2d2adf50c0335eb0000000000000000000000001e86ae93a09895ffdf65dbc30044a97266fa12c400000000000000000000000075f0c86c268ba51aff3a78cb8696ee350ba8bfde00000000000000000000000015e1c04e9c5ca82e345e9fe549d0e959f15695c70000000000000000000000006201f32c4a95c3b9c64248e76b8fceeb856458640000000000000000000000005b65b896918ebd74f843d0dcfd1e7bf658a9ccd0000000000000000000000000fb95a069bca28020ee26ee9249b8c8c3e39de19e0000000000000000000000002826e0a4cfe5fcec5279aec12c4c928d545f8e3b00000000000000000000000047ff9f6d8b60d7bf7b8b162fd15ff7466c030639000000000000000000000000347691621e017447144fdec87cdcc11a18e0c103000000000000000000000000c941fc5ef8613f0a6fc1928a1310f9f189b358830000000000000000000000004ee081fd9a51c1049164a047b0392b6eacf653bc0000000000000000000000008528520487cc55be4a0c1107245e64cc8c71ca930000000000000000000000000ee876b94c9b78f47a52ba13faf1717a0366d5f500000000000000000000000029bebb64d4ef42be9b84fdd220a3249d3af8af9a000000000000000000000000a3ed2f67fffe1bfd47acf19b2e87b642b6d9372500000000000000000000000057966d342f21a43159652e32ddf6daf55a21fbe20000000000000000000000002fe2d27315d598298bf2450da927ea0f5a8d280300000000000000000000000061364d8034eca6a67382873d6df7a3e2272fa95b0000000000000000000000007739d16c64bcd859b17a43086a037fa56f4c0c1500000000000000000000000032213e3cb433aa8387c8bf5352c8b6fb9a4721ae000000000000000000000000b7f4ed0af44cf6b136cae29d6438832ed51f26b600000000000000000000000068c9a24fc41881daf1a82c1d59048270cc510dac000000000000000000000000c61c871aca1ee6d97221f38192ce7a1e04716eed00000000000000000000000029a7344119c4fd3e582b5e1454a1448a35ef243a0000000000000000000000000b56d716e765c454f3065d7686495eef408371220000000000000000000000002e0f50ea52213cebc28706776a07a97f4796d88d000000000000000000000000bf2c8d2f98a4dbc05f51f475d81c48b3efb7dcba0000000000000000000000004d6954860a5291a60d64ac9f2c224b742dee40730000000000000000000000001ff00121e9f6f4839b08a6efe6348f0bcc9ecd5c0000000000000000000000009029c8a11e310f1abbcfcfdac4dae7fa3cf0f56b000000000000000000000000aa3cfe869ddb8c505ea32e7856aba47c511eef660000000000000000000000002725f5d4ff99d5cc8f20211dabb832c5fbd4f2d8000000000000000000000000a6c7ec7b289fdece521dbaafa274038bb95c5fcf000000000000000000000000e522124f4c57f0976ca00a7ab9125ce8ab6d1d09000000000000000000000000fa39b9dfc6f613217126c34206586d4bc6af3852000000000000000000000000ea2c0ae279e26079926886770b5710a95dc9d86c000000000000000000000000a20d921cec2a6c56d1ecc32e548aa713e2550fe30000000000000000000000006b090ee37ead372281fbedb5d8636796c6c89d35000000000000000000000000a2f266a1cc103b8ac1dcf564c5fae262c9fc8364000000000000000000000000e82fe8fd7beaecbaf02f0879f9cc7fb592e3d0c30000000000000000000000002e13ab492cc3001436ad48be416023ecaf0072c00000000000000000000000006c2e89dd9c3539f3c87f2e31244b664b92c85883000000000000000000000000aab8b753b05c90ec704e9dbe79a9e366f1a4775c000000000000000000000000eaf958a6d7bddfc4b1b5fb0b880b2a998df146850000000000000000000000009ef206fa56f673b57260654183398d1c7e210e2a000000000000000000000000cc9973acb08ee36ce1dd57eb94263449f722763000000000000000000000000019758c4247a22882fcc03f1c764c69774d56a610000000000000000000000000f177b67775e4b8b8b723d94a1e4b300c29c676d0000000000000000000000000be1558074111404da810fd48654941d522ee845d000000000000000000000000ba2d13ae3212781520f11d147fea508d8cf44056000000000000000000000000a5e09eba16c94ec69c24c2785dd7065289146cca00000000000000000000000080e88bb37de082057d587600a7efc1561d86d191000000000000000000000000be378a8d91a14e0cdaa61796509a75ee8255bfce0000000000000000000000005170e50ff672a6431293ce9c693b0ea7d1d2990d000000000000000000000000c98f8e16d9a40477c4053f887b828f1231cdab7a000000000000000000000000f2e67535aa6b4fafbd6e39ab65742542bd3d6780000000000000000000000000a6ef8ad6c76bb33b63f9619953b3584d30654c49000000000000000000000000a66c6cfb6b745a30ce0a18f9cca662a1a78d78d0000000000000000000000000ec287f89381b8fef25858e2854d3c6dfc2ee7ea1000000000000000000000000f50fee3a8196e49bfb6501e86411936ecb03e952000000000000000000000000644f41f35e07a3699df4366808ac08ca1037863b000000000000000000000000c5f5c91c036d2379f2e3ace8a3310246addd7554000000000000000000000000f068395f1adbe0d6f3b3e6c20d550b7a568be369000000000000000000000000018ad443331c1bc8af419e1ab8813e37dbf07b3e000000000000000000000000ca88eb1690dede759e2cbc0261a2601eb49352c9000000000000000000000000dceae123189d87008c87447fdd9466de64674baf000000000000000000000000ae880cbbc6739a153c5fc61aa2dcab963da2656b000000000000000000000000ab717ecbf05da67b55fda9fa23c330fb4afc93a2000000000000000000000000476ee4afa9112842c5f6d7014b6eb1c08f2f339100000000000000000000000003347f84d1a562fd978e05a0f6a37760b271d340000000000000000000000000b802553dc8afc397294c74af9ee04683bc1ae72d00000000000000000000000042d2ae06e68d447eaa5d5978b3ea8e66bf27ab53000000000000000000000000041609c83a2c8e1062f71ce15b4a1f2fff42ffba0000000000000000000000005e9d7face1d1369ac9069cea31336061992b267e000000000000000000000000088d52971c9802823e538a898847e5040c9c76190000000000000000000000008b27f703c1188aeb17d9f7f7d1f99ea7a26f8fb100000000000000000000000014cca55297ebf43fba0ab23613850a0c4d15376b000000000000000000000000c0f9cb7e611f6b9822654909bf6abbef0ccc255b000000000000000000000000d7a2dc21b878757a0cfc915f60a465fa595993f7000000000000000000000000a942d95aa06eb8dfaaf2e7d06f85f026e307810f00000000000000000000000021a415f52a87c12cb3163db935ea929d0843b48e0000000000000000000000000cf5deb1e629b8a0f33685babca4157b62a2336500000000000000000000000076befa597d53035ee0d43eab11e129f198a3b09400000000000000000000000000385aa52a301c030b166d8981c6917aa57a70020000000000000000000000006b668cb92c124dc26a9b4ba9fda27c0a8a60f8f60000000000000000000000009ae1d82fbb009e7bb2afe2547e4b3b6fe45342b3000000000000000000000000435bf4a1c73a382b92816c344ca2ac5e7e9975d200000000000000000000000033a4aa3cc0435ac3d247b62e03797c0a31d3229f0000000000000000000000002638db06e731c4b55afce63e1d933189acb431c70000000000000000000000007a947ac4e196397d91427d245c43e8f99a17c2ea000000000000000000000000b4fa647a23af5dfcc424475de99958063cb0e148000000000000000000000000e766bf1dc39ec15ca80267900d17c1442b544efc000000000000000000000000a860eefe8a03662c7348bb4529781dd21406244100000000000000000000000063cd05475910473ffccd41e729e4068b45dc8d510000000000000000000000002995f0a68fc4f51048f307bf081b8c09a0e4e99700000000000000000000000011222a31be36a6d853fb852a9a44ddb241290b08000000000000000000000000f58db63ae989b13a7ea02743417a1b58defd66740000000000000000000000001ab5971b8d837f4eb082e2c7d262292b9d82e1ff000000000000000000000000479cf15609cc892ecbeab83499a86c14c99c3bab000000000000000000000000ca90af695f8c87dbb080e0a73a6e1b97413a7b59000000000000000000000000329f234c378cd3e236fabb733f193d4008a5cd6d000000000000000000000000f260d61ed3c180539f921048983b3832a69d811a000000000000000000000000149d2fcc8eda76a098966622fc24f36cd74d538700000000000000000000000076cfffa7139903ba7233409eb2cd22c23d4c19fb000000000000000000000000ec65d4e35ca9f9ecaef3a2ffd64df1a0ac9d5be80000000000000000000000000f2556cabed056026f30fd55ee4b4e7c0c2f68b40000000000000000000000002cc1e26bbf1a53695110d78faa7b021aa2c8f20a00000000000000000000000086b6ec7076171530692cb325d94a1425148da97d0000000000000000000000004ac6dc39545947364b416937f6431ba1818f4fbb000000000000000000000000ad3dc8d2d8cf0abc333bcadcd2ffb7cafb912b910000000000000000000000001d6ff73049d71356ee20c5b403c641915faa4de2000000000000000000000000b7267f458b0d4f38744466590a8023a4a7e7ea56000000000000000000000000e56bcc23776410ab3b32ab0497b34154b73ff8b50000000000000000000000001bde8c21b47be99d58c43d283f7501703a64ab0b000000000000000000000000ced24b6a49c5217dd4f9cae19ac5cc2d7df38f61000000000000000000000000785bf7b0235908ba76207081aec8ba4ce361524e000000000000000000000000f0429528ded30121f9c2ab0d4de653d51becedf400000000000000000000000051b21605571c987ca5cb03f03ec7ee7206704af500000000000000000000000070a50c0e174a5e9f4a56e2ae849fcac97199dee9000000000000000000000000d7da71e7ddc95e464607efeac6c6c6d46a26f8610000000000000000000000000a7ffd571a8a2430d10095173dd94c96ed076abd00000000000000000000000031d18eed98e7d819644531252b8caae84b9e1771000000000000000000000000684e2a76cb3281bb3e9dc3e7d5a29151a1f4e160000000000000000000000000d9561a4546e0da11dc8ebe4ab5e02dc61fcd3e3a000000000000000000000000075773cdac7408e3e72919aefb543314c0015943000000000000000000000000ba0267881b1de1aec47ea0418d6c8ad20ef192290000000000000000000000003cff55c18176205ab656481921270b04939426b1000000000000000000000000f5691a5f8afb2c7bb618aa346085832bdd3d6a4a000000000000000000000000a8e5a934c85c4a07a8bf4b0d2320cabd33b59d5d000000000000000000000000f08b880a782d8c4083baa1073b0de22bf67c7d4c0000000000000000000000002776ca69dd33bce8b1cec3e6a51c7268313cb6d9000000000000000000000000847f1db65ce5e9232142c0ca5493c8df09035ef80000000000000000000000000ee44f027548fdcce4174e4471733346222cf446000000000000000000000000af87f44937eda988b6b1cce54d71fd2a6ae25ece000000000000000000000000a9293285ad996d39e187e265bf19ff9f6d58c2b3000000000000000000000000f2537e61d09eba3c0a1e3d2c5ee118deab383343000000000000000000000000974f61efc0dc1bfcfc86312caf88b7881c69fe9d00000000000000000000000076a38674d9bccab9400f06e01b8493cacaed54940000000000000000000000005bfe4e0780057bfb9553818febf3ae87424804fd000000000000000000000000209fdd51c58a081891c615adf5f1b4d59266e4e2000000000000000000000000ba324d55462b0822d9aab6cab88ab12bf7376913000000000000000000000000ad38221bede1e10715c406b89dddd263e85c22c1000000000000000000000000c67ddd52e383df02e3365d7f8569c8a439181d7a0000000000000000000000001261ef5836770e577acee2c1f235339b0639a24a000000000000000000000000d38910fe35774693713c748b98e59ca99acd028d00000000000000000000000072824bb44b35eab26e88e9e1f5e33e2ca66d761100000000000000000000000099ed616b50061b18acfd3aebcc40f6d5053c0afd00000000000000000000000019c1314ddb8089cf54ccfc9743ed4f6b4de16bc3000000000000000000000000af6e61096b8f2ec43f8bb8f74465df5ed0584189000000000000000000000000f55c26bc0da0a50677abb0329344d63e9791cc64000000000000000000000000a7790d6f5a9b708d77d2989ad7cb9403ba215d9d0000000000000000000000002f8c738d0dca8a63d5384ac9271c3b6c2f7b82e8000000000000000000000000735540a4e419242624e333401ed0cff11d8e394c000000000000000000000000fc61dde6fcd54d2d602298097a92810752ea2216000000000000000000000000a474f7df19c449350de2d8ed9530a96bd489e6180000000000000000000000007e1e3abd8dd50a480f1e94ace42024809a9976b00000000000000000000000007a5b3261d1e318b2b1d7cd8395fec2d114cf1e0700000000000000000000000022b8526cb9bf35870fb3b64d2830a342662fae1a000000000000000000000000273c1e818ad4323b261e6989adcd6ec292f39eb6000000000000000000000000b8bc2b71537cdbe0cbbebe3743d456751e48b437000000000000000000000000e5eb897aee540b3b61f9657ae6c7b3a8817e5bbc00000000000000000000000076bc03f1e645601bb46e1b913ad19f5cc68ead9e0000000000000000000000004e907e40d9e14d137ca9a4dc4e837526ab24a75a0000000000000000000000003e872672b1aa1928e2c595c792d3660bdb9bf009000000000000000000000000002c24046f6238c8a23d5e66af11e82b413a747300000000000000000000000016ed4972d859c4863f5aac4827ed35082fefe2dd00000000000000000000000093d7cd3e4b5b02a57ac94536c49e09b3b678298c0000000000000000000000002fe6d5d36fae80f36b49c41cc669495a15d61f1c000000000000000000000000ee336bb5175862703e2d121b2d62fe3230defa8d000000000000000000000000ca40f22df970cfac0fb548f4681544c0fb76d2260000000000000000000000004a0e6f7c6a68b009549f7022fc772f04f0b3d267000000000000000000000000500739e1734d4361a642e3dc1947eab3312cbe020000000000000000000000007f9777b9460f6f2e2fec0737b3ecb764135569cb00000000000000000000000054e8086deb08c761eaa0aa77b6b0846c9d2ffbb2000000000000000000000000c54885669813cabbf2809e1abfc8f64ff963081a000000000000000000000000cebe744ffe69ba5c903a648a9ee1e7951d06d35c000000000000000000000000c4b4c4c30ee327faf976fea34d91ada6dc65aae3000000000000000000000000cd52cd7fe07189bd16d236eb1da587bb9d2fbc690000000000000000000000009ac44065540cb31b69c9f18c22392c6eb6cbb7b30000000000000000000000000a791b8d6c823a58f59782bf901141d2e21d3867000000000000000000000000dc75d344dfcf394da9ef25500b83551809fd7c23000000000000000000000000d76a3139322bf6edcdd8e71cbe225a80c94a43d000000000000000000000000031a3135956b33f9936791e5f11546e7f9a11d91f00000000000000000000000067a045c776f4422d56d6f59c70fb42bba05be7c9000000000000000000000000f3e27a226c9c1c0169119dad4c6f8ce8b07342b2000000000000000000000000eaebad11a448fc6b3fdb40f53bbbcaefc9e12720000000000000000000000000715b03b71847e76ddef8ac19dc0bdf92f2fee7630000000000000000000000005032f427d9b911ebe63ad99f707e1fafa7b9a23b0000000000000000000000001758fe2c3492bed3f6e2a95247b6df04fbdc4cfc00000000000000000000000016aad9880d1504fef8b64ed16ee33e7d42bd9321000000000000000000000000edc03dda760e5793ecda69d9837956f246e2cc7c0000000000000000000000006a89f27f7dee87a4b69167ca795dbe07478de68900000000000000000000000091d762a023ee879222728725d1b4cfeca45aef1e00000000000000000000000046a3d887a93389e59c1081a96c23578a8a97e87100000000000000000000000072da397dfb39e9bdaf2e6e5852886a8e0f422b07000000000000000000000000e2c8c2258ab538d28501e23e938310946df84ac1000000000000000000000000bb022fa11c82a529f67d850029ae9ba111d6ae580000000000000000000000005dc5002fe446e7526aca4c30068f162ff806953b0000000000000000000000002e254caaf6662a65f28d0e6826e23305897686eb0000000000000000000000002da4b85b57ed5445eb9d92190f1cc31f70791cc7000000000000000000000000127f2b9ba900b3698589d66e22b457dcc2e988f00000000000000000000000001c5f2610033871625f91ef349ff5ad29168a5f4f000000000000000000000000381c1fbbf9821e2d3af022d9bfcd38a223d8d733000000000000000000000000af9ca6bf95489773edb5003abf3f96e137b0dce00000000000000000000000002187b84a100effa2c5c5bf6d31a959b9338a799d000000000000000000000000922875f3ac04bc4ee3f249216d83f5a3b30a1f83000000000000000000000000430d3d400469a788bba266a718916c886400c68e000000000000000000000000f2d811c2ac3e8019c18ff3530777c072cc8a580a000000000000000000000000dc5c5b4ef174c2c57fe6c712851729fa86496622000000000000000000000000e59e04766142949d054abfa7a9ae29147a2dbde5000000000000000000000000616e72cb01e23dfb60b412580fa26739185373ca0000000000000000000000005a1e7ef74e0fd75a2ee88cd6cec34ef3362efd0600000000000000000000000056f857ba48c51822a9ee3f0e658c608bc26732b10000000000000000000000008143cbd82f9329f112076510706639c58e7fee2f000000000000000000000000cb00f2d13d7840c4b60b995edc76d250e2bce4b600000000000000000000000016bdc0f3a107af309153772089298277891026d4000000000000000000000000d21fd880f77730e3fe563a3b884e2231426d423e0000000000000000000000000b2eed6a84dd1ba7d8ece83a1665bdb5a4428b94000000000000000000000000ab1ca7e2322b23c5a51c9704684efd8ce32a799e00000000000000000000000091e5e85ee93ca51b26137cb0bafd67ba9de477cc0000000000000000000000006ff0825bc7d48865078fe3413e4be66bd41677e0000000000000000000000000794cdc7d28cda9720186e8f81c57a084eef049b200000000000000000000000000000000000000000000000000000000000002bc0000000000000000000000000000000000000000000000000000005e627181b400000000000000000000000000000000000000000000000000002bc6d43b0bd200000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000e22aaf624700000000000000000000000000000000000000000000000000000843a372ad7e00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000017ee175821700000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000ecda0e58cf00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000001726a3162700000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000002bb6cd2c9a060000000000000000000000000000000000000000000000000000000e3f2948b50000000000000000000000000000000000000000000000000000001396d8c3fa0000000000000000000000000000000000000000000000000000001726a3162700000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000031dd107e7c00000000000000000000000000000000000000000000000000000011cef39ae3000000000000000000000000000000000000000000000000000001f7fa546c210000000000000000000000000000000000000000000000000000006ad9b5a153000000000000000000000000000000000000000000000000000000d05bbbc76300000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000903f82003100000000000000000000000000000000000000000000000000000051eb2d62150000000000000000000000000000000000000000000000000000019997e2ea6c0000000000000000000000000000000000000000000000000000001396d8c3fa000000000000000000000000000000000000000000000000000000f5c188264100000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000001597ba9233a00000000000000000000000000000000000000000000000000000010070e71cc0000000000000000000000000000000000000000000000000000004903b394a40000000000000000000000000000000000000000000000000000004e5b630fe8000000000000000000000000000000000000000000000000000000b215840ce10000000000000000000000000000000000000000000000000000015b438e4c5000000000000000000000000000000000000000000000000000000051eb2d621500000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000005742dcdd5900000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000590ac206700000000000000000000000000000000000000000000000000000007ca8a93c370000000000000000000000000000000000000000000000000000005c9a8c589e0000000000000000000000000000000000000000000000000000001396d8c3fa000000000000000000000000000000000000000000000000000000602a56aacb000000000000000000000000000000000000000000000000000000155ebded1000000000000000000000000000000000000000000000000000000073c12f6ec5000000000000000000000000000000000000000000000000000000557af7b443000000000000000000000000000000000000000000000000000000c93c2723080000000000000000000000000000000000000000000000000000006911d0783d000000000000000000000000000000000000000000000000000000f95152786e00000000000000000000000000000000000000000000000000000051eb2d621500000000000000000000000000000000000000000000000000000167bad26bf00000000000000000000000000000000000000000000000000000001ab66d685400000000000000000000000000000000000000000000000000000051eb2d6215000000000000000000000000000000000000000000000000000000356cdad0a9000000000000000000000000000000000000000000000000000000cccbf17536000000000000000000000000000000000000000000000000000000d3eb861991000000000000000000000000000000000000000000000000000000eb12292fb80000000000000000000000000000000000000000000000000000015ed3589e7e0000000000000000000000000000000000000000000000000000005ad2a72f8700000000000000000000000000000000000000000000000000002bca64055e000000000000000000000000000000000000000000000000000000003c8c6f750400000000000000000000000000000000000000000000000000000031dd107e7c0000000000000000000000000000000000000000000000000000002c856103380000000000000000000000000000000000000000000000000000004acb98bdba000000000000000000000000000000000000000000000000000000c77441f9f2000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000001ab66d685400000000000000000000000000000000000000000000000000000107907bc124000000000000000000000000000000000000000000000000000000eea1f381e6000000000000000000000000000000000000000000000000000000155ebded1000000000000000000000000000000000000000000000000000000010070e71cc00000000000000000000000000000000000000000000000000000041e41ef049000000000000000000000000000000000000000000000000000000401c39c73200000000000000000000000000000000000000000000000000000043ac04195f0000000000000000000000000000000000000000000000000000004acb98bdba000000000000000000000000000000000000000000000000000000239de735c60000000000000000000000000000000000000000000000000000008e779cd71a000000000000000000000000000000000000000000000000000000d223a0f07a0000000000000000000000000000000000000000000000000000018990d4789f000000000000000000000000000000000000000000000000000000e7825edd8b0000000000000000000000000000000000000000000000000000004c937de6d1000000000000000000000000000000000000000000000000000000f069d8aafc000000000000000000000000000000000000000000000000000000e5ba79b47400000000000000000000000000000000000000000000000000000187c8ef4f890000000000000000000000000000000000000000000000000000003c8c6f750400000000000000000000000000000000000000000000000000000033a4f5a793000000000000000000000000000000000000000000000000000000de9ae51019000000000000000000000000000000000000000000000000000001a60f270a0b000000000000000000000000000000000000000000000000000000590ac206700000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000473bce6b8d00000000000000000000000000000000000000000000000000000155ebded10c00000000000000000000000000000000000000000000000000000087580832bf0000000000000000000000000000000000000000000000000000001ab66d6854000000000000000000000000000000000000000000000000000000473bce6b8d000000000000000000000000000000000000000000000000000001726a316278000000000000000000000000000000000000000000000000000000cb040c4c1f0000000000000000000000000000000000000000000000000000018b58b9a1b6000000000000000000000000000000000000000000000000000000f231bdd41300000000000000000000000000000000000000000000000000000165f2ed42d9000000000000000000000000000000000000000000000000000000473bce6b8d0000000000000000000000000000000000000000000000000000016b4a9cbe1d0000000000000000000000000000000000000000000000000000016982b7950600000000000000000000000000000000000000000000000000000053b3128b2c0000000000000000000000000000000000000000000000000000017432168b8f000000000000000000000000000000000000000000000000000000d3eb861991000000000000000000000000000000000000000000000000000000ecda0e58cf000000000000000000000000000000000000000000000000000000e94a4406a200000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000038fca522d7000000000000000000000000000000000000000000000000000000e94a4406a2000000000000000000000000000000000000000000000000000001726a316278000000000000000000000000000000000000000000000000000000eea1f381e6000000000000000000000000000000000000000000000000000000e062ca3930000000000000000000000000000000000000000000000000000000d9433594d50000000000000000000000000000000000000000000000000000010400b16ef7000000000000000000000000000000000000000000000000000000d5b36b42a70000000000000000000000000000000000000000000000000000019b5fc81383000000000000000000000000000000000000000000000000000000658206260f0000000000000000000000000000000000000000000000000000002abd7bda2100000000000000000000000000000000000000000000000000000157b3c3fa230000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000557af7b4430000000000000000000000000000000000000000000000000000018b58b9a1b60000000000000000000000000000000000000000000000000000018ee883f3e40000000000000000000000000000000000000000000000000000010070e71cc900000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000001ab66d685400000000000000000000000000000000000000000000000000000187c8ef4f8900000000000000000000000000000000000000000000000000000030152b556500000000000000000000000000000000000000000000000000000170a24c39610000000000000000000000000000000000000000000000000000009207672948000000000000000000000000000000000000000000000000000000d05bbbc7630000000000000000000000000000000000000000000000000000006749eb4f260000000000000000000000000000000000000000000000000000008cafb7ae030000000000000000000000000000000000000000000000000000017432168b8f000000000000000000000000000000000000000000000000000000038fca522d0000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000db0b1abdec0000000000000000000000000000000000000000000000000000001396d8c3fa00000000000000000000000000000000000000000000000000000186010a2672000000000000000000000000000000000000000000000000000000e062ca3930000000000000000000000000000000000000000000000000000001609b3dc795000000000000000000000000000000000000000000000000000000e3f2948b5d000000000000000000000000000000000000000000000000000000c77441f9f2000000000000000000000000000000000000000000000000000000db0b1abdec0000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000f95152786e0000000000000000000000000000000000000000000000000000002565cc5edd00000000000000000000000000000000000000000000000000000177c1e0ddbc000000000000000000000000000000000000000000000000000000557af7b4430000000000000000000000000000000000000000000000000000018990d4789f000000000000000000000000000000000000000000000000000000fb1937a1850000000000000000000000000000000000000000000000000000007750f9c0f300000000000000000000000000000000000000000000000000000050234838fe00000000000000000000000000000000000000000000000000000087580832bf0000000000000000000000000000000000000000000000000000005ad2a72f870000000000000000000000000000000000000000000000000000010238cc45e0000000000000000000000000000000000000000000000000000000d3eb861991000000000000000000000000000000000000000000000000000000fb1937a18500000000000000000000000000000000000000000000000000000038fca522d7000000000000000000000000000000000000000000000000000000e5ba79b4740000000000000000000000000000000000000000000000000000004e5b630fe8000000000000000000000000000000000000000000000000000000590ac20670000000000000000000000000000000000000000000000000000000be8cc82c80000000000000000000000000000000000000000000000000000000f231bdd4130000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000f069d8aafc000000000000000000000000000000000000000000000000000000557af7b443000000000000000000000000000000000000000000000000000000eea1f381e60000000000000000000000000000000000000000000000000000015b438e4c5000000000000000000000000000000000000000000000000000000182713fd4450000000000000000000000000000000000000000000000000000000c77441f9f0000000000000000000000000000000000000000000000000000017989c606d3000000000000000000000000000000000000000000000000000001324df79b4600000000000000000000000000000000000000000000000000000071f94a45ae00000000000000000000000000000000000000000000000000000075891497dc00000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000001726a3162780000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000cccbf17536000000000000000000000000000000000000000000000000000000d77b506bbe0000000000000000000000000000000000000000000000000000016982b79506000000000000000000000000000000000000000000000000000000f5c1882641000000000000000000000000000000000000000000000000000000f95152786e0000000000000000000000000000000000000000000000000000005ad2a72f87000000000000000000000000000000000000000000000000000000ecda0e58cf00000000000000000000000000000000000000000000000000000075891497dc000000000000000000000000000000000000000000000000000000f7896d4f5700000000000000000000000000000000000000000000000000000053b3128b2c000000000000000000000000000000000000000000000000000000fb1937a1850000000000000000000000000000000000000000000000000000006e697ff3810000000000000000000000000000000000000000000000000000016b4a9cbe1d00000000000000000000000000000000000000000000000000000053b3128b2c00000000000000000000000000000000000000000000000000000063ba20fcf9000000000000000000000000000000000000000000000000000000c93c27230800000000000000000000000000000000000000000000000000000063ba20fcf9000000000000000000000000000000000000000000000000000001843924fd5b000000000000000000000000000000000000000000000000000000557af7b4430000000000000000000000000000000000000000000000000000005ad2a72f8700000000000000000000000000000000000000000000000000000170a24c396100000000000000000000000000000000000000000000000000000175f9fbb4a5000000000000000000000000000000000000000000000000000000f069d8aafc0000000000000000000000000000000000000000000000000000005c9a8c589e000000000000000000000000000000000000000000000000000000ce93d69e4d00000000000000000000000000000000000000000000000000000043ac04195f000000000000000000000000000000000000000000000000000000ce93d69e4d000000000000000000000000000000000000000000000000000000038fca522d0000000000000000000000000000000000000000000000000000004e5b630fe80000000000000000000000000000000000000000000000000000008cafb7ae0300000000000000000000000000000000000000000000000000000011cef39ae3000000000000000000000000000000000000000000000000000000bcc4e303690000000000000000000000000000000000000000000000000000003ac48a4bee000000000000000000000000000000000000000000000000000000356cdad0a9000000000000000000000000000000000000000000000000000000d223a0f07a0000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000d223a0f07a0000000000000000000000000000000000000000000000000000007031651c980000000000000000000000000000000000000000000000000000005c9a8c589e00000000000000000000000000000000000000000000000000000051eb2d62150000000000000000000000000000000000000000000000000000005c9a8c589e00000000000000000000000000000000000000000000000000000001c7e52916000000000000000000000000000000000000000000000000000000ce93d69e4d000000000000000000000000000000000000000000000000000000dcd2ffe7020000000000000000000000000000000000000000000000000000002abd7bda21000000000000000000000000000000000000000000000000000000d05bbbc763000000000000000000000000000000000000000000000000000000557af7b4430000000000000000000000000000000000000000000000000000001396d8c3fa0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001396d8c3fa0000000000000000000000000000000000000000000000000000004e5b630fe80000000000000000000000000000000000000000000000000000010238cc45e0000000000000000000000000000000000000000000000000000000d3eb861991000000000000000000000000000000000000000000000000000000fb1937a1850000000000000000000000000000000000000000000000000000008cafb7ae03000000000000000000000000000000000000000000000000000000f95152786e0000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000d9433594d50000000000000000000000000000000000000000000000000000005ad2a72f870000000000000000000000000000000000000000000000000000001726a3162700000000000000000000000000000000000000000000000000000187c8ef4f890000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000017432168b8f00000000000000000000000000000000000000000000000000000053b3128b2c000000000000000000000000000000000000000000000000000000de9ae510190000000000000000000000000000000000000000000000000000004e5b630fe8000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000006e697ff381000000000000000000000000000000000000000000000000000000b04d9ee3ca000000000000000000000000000000000000000000000000000001642b0819c2000000000000000000000000000000000000000000000000000000ecda0e58cf00000000000000000000000000000000000000000000000000000092076729480000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000c5ac5cd0db00000000000000000000000000000000000000000000000000000149749ab16d0000000000000000000000000000000000000000000000000000006ad9b5a153000000000000000000000000000000000000000000000000000000b5a54e5f0e00000000000000000000000000000000000000000000000000000051eb2d6215000000000000000000000000000000000000000000000000000000155ebded10000000000000000000000000000000000000000000000000000000f231bdd413000000000000000000000000000000000000000000000000000000eea1f381e60000000000000000000000000000000000000000000000000000002e4d462c4f0000000000000000000000000000000000000000000000000000006ca19aca6a00000000000000000000000000000000000000000000000000000051eb2d621500000000000000000000000000000000000000000000000000000197cffdc155000000000000000000000000000000000000000000000000000000e062ca39300000000000000000000000000000000000000000000000000000017ee175821700000000000000000000000000000000000000000000000000000063ba20fcf9000000000000000000000000000000000000000000000000000000cb040c4c1f000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000001ab66d6854000000000000000000000000000000000000000000000000000000d77b506bbe00000000000000000000000000000000000000000000000000000105c896980d000000000000000000000000000000000000000000000000000000e3f2948b5d0000000000000000000000000000000000000000000000000000008ae7d284ed000000000000000000000000000000000000000000000000000000ecda0e58cf000000000000000000000000000000000000000000000000000000de9ae51019000000000000000000000000000000000000000000000000000000b5a54e5f0e0000000000000000000000000000000000000000000000000000016b4a9cbe1d0000000000000000000000000000000000000000000000000000016b4a9cbe1d00000000000000000000000000000000000000000000000000000033a4f5a7930000000000000000000000000000000000000000000000000000005c9a8c589e0000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000155ebded10000000000000000000000000000000000000000000000000000000590ac20670000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000017d19905900000000000000000000000000000000000000000000000000000000d77b506bbe0000000000000000000000000000000000000000000000000000004903b394a4000000000000000000000000000000000000000000000000000000b215840ce100000000000000000000000000000000000000000000000000000031dd107e7c000000000000000000000000000000000000000000000000000000e94a4406a2000000000000000000000000000000000000000000000000000000557af7b44300000000000000000000000000000000000000000000000000000053b3128b2c0000000000000000000000000000000000000000000000000000008038738e64000000000000000000000000000000000000000000000000000000e7825edd8b0000000000000000000000000000000000000000000000000000017989c606d3000000000000000000000000000000000000000000000000000000de9ae51019000000000000000000000000000000000000000000000000000000f3f9a2fd2a0000000000000000000000000000000000000000000000000000004e5b630fe8000000000000000000000000000000000000000000000000000000be8cc82c80000000000000000000000000000000000000000000000000000000d9433594d50000000000000000000000000000000000000000000000000000005e627181b40000000000000000000000000000000000000000000000000000008038738e6400000000000000000000000000000000000000000000000000000050234838fe00000000000000000000000000000000000000000000000000000041e41ef0490000000000000000000000000000000000000000000000000000016b4a9cbe1d0000000000000000000000000000000000000000000000000000014b3c7fda840000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000155ebded10000000000000000000000000000000000000000000000000000001609b3dc7950000000000000000000000000000000000000000000000000000001ab66d68540000000000000000000000000000000000000000000000000000008cafb7ae0300000000000000000000000000000000000000000000000000000041e41ef049000000000000000000000000000000000000000000000000000000891fed5bd60000000000000000000000000000000000000000000000000000015423f9a7f6000000000000000000000000000000000000000000000000000000c054ad5597000000000000000000000000000000000000000000000000000000ecda0e58cf000000000000000000000000000000000000000000000000000000590ac2067000000000000000000000000000000000000000000000000000000197cffdc1550000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000007ae0c41320000000000000000000000000000000000000000000000000000000d77b506bbe000000000000000000000000000000000000000000000000000000fb1937a18500000000000000000000000000000000000000000000000000000051eb2d6215000000000000000000000000000000000000000000000000000000590ac20670000000000000000000000000000000000000000000000000000000acbdd4919d0000000000000000000000000000000000000000000000000000017989c606d300000000000000000000000000000000000000000000000000000043ac04195f000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000004e5b630fe80000000000000000000000000000000000000000000000000000007e708e654e000000000000000000000000000000000000000000000000000000155ebded1000000000000000000000000000000000000000000000000000000170a24c3961000000000000000000000000000000000000000000000000000000602a56aacb0000000000000000000000000000000000000000000000000000019d27ad3c9a000000000000000000000000000000000000000000000000000000e062ca393000000000000000000000000000000000000000000000000000000187c8ef4f89000000000000000000000000000000000000000000000000000000e5ba79b474000000000000000000000000000000000000000000000000000001195f6f5c0700000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000008038738e640000000000000000000000000000000000000000000000000000001396d8c3fa000000000000000000000000000000000000000000000000000000d5b36b42a70000000000000000000000000000000000000000000000000000005ad2a72f8700000000000000000000000000000000000000000000000000000170a24c39610000000000000000000000000000000000000000000000000000015b438e4c500000000000000000000000000000000000000000000000000000007031651c980000000000000000000000000000000000000000000000000000004c937de6d1000000000000000000000000000000000000000000000000000000d5b36b42a700000000000000000000000000000000000000000000000000000031dd107e7c000000000000000000000000000000000000000000000000000000fea901f3b2000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000005742dcdd590000000000000000000000000000000000000000000000000000006749eb4f2600000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000000e7825edd8b0000000000000000000000000000000000000000000000000000006ad9b5a153000000000000000000000000000000000000000000000000000000e5ba79b47400000000000000000000000000000000000000000000000000000187c8ef4f890000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000004573e94276000000000000000000000000000000000000000000000000000000a0469071fd000000000000000000000000000000000000000000000000000000b76d338825000000000000000000000000000000000000000000000000000000602a56aacb0000000000000000000000000000000000000000000000000000006911d0783d0000000000000000000000000000000000000000000000000000005c9a8c589e000000000000000000000000000000000000000000000000000000a92e0a3f6f0000000000000000000000000000000000000000000000000000002e4d462c4f000000000000000000000000000000000000000000000000000000975f16a48c0000000000000000000000000000000000000000000000000000001ab66d685400000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000007750f9c0f300000000000000000000000000000000000000000000000000000051eb2d62150000000000000000000000000000000000000000000000000000016b4a9cbe1d000000000000000000000000000000000000000000000000000000dcd2ffe7020000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000e94a4406a2000000000000000000000000000000000000000000000000000000e94a4406a2000000000000000000000000000000000000000000000000000000d3eb8619910000000000000000000000000000000000000000000000000000005c9a8c589e0000000000000000000000000000000000000000000000000000006ca19aca6a0000000000000000000000000000000000000000000000000000005e627181b400000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000073c12f6ec50000000000000000000000000000000000000000000000000000010ce82b3c6800000000000000000000000000000000000000000000000000000073c12f6ec500000000000000000000000000000000000000000000000000000105c896980d00000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000001396d8c3fa000000000000000000000000000000000000000000000000000000d223a0f07a00000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000be8cc82c80000000000000000000000000000000000000000000000000000000975f16a48c000000000000000000000000000000000000000000000000000000e5ba79b474000000000000000000000000000000000000000000000000000000d5b36b42a7000000000000000000000000000000000000000000000000000000e3f2948b5d0000000000000000000000000000000000000000000000000000007918deea09000000000000000000000000000000000000000000000000000000f5c18826410000000000000000000000000000000000000000000000000000003ac48a4bee000000000000000000000000000000000000000000000000000000b3dd6935f80000000000000000000000000000000000000000000000000000002565cc5edd000000000000000000000000000000000000000000000000000000eea1f381e600000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000000cccbf17536000000000000000000000000000000000000000000000000000000a59e3fed42000000000000000000000000000000000000000000000000000000b04d9ee3ca000000000000000000000000000000000000000000000000000000de9ae51019000000000000000000000000000000000000000000000000000000e3f2948b5d0000000000000000000000000000000000000000000000000000017d1990590000000000000000000000000000000000000000000000000000000053b3128b2c000000000000000000000000000000000000000000000000000000eea1f381e6000000000000000000000000000000000000000000000000000000e3f2948b5d0000000000000000000000000000000000000000000000000000005742dcdd59000000000000000000000000000000000000000000000000000000d5b36b42a70000000000000000000000000000000000000000000000000000019d27ad3c9a000000000000000000000000000000000000000000000000000000cccbf17536000000000000000000000000000000000000000000000000000001408d20e3fb000000000000000000000000000000000000000000000000000000891fed5bd6000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000000e3f2948b5000000000000000000000000000000000000000000000000000000239de735c6000000000000000000000000000000000000000000000000000000c93c2723080000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000e22aaf62470000000000000000000000000000000000000000000000000000007918deea09000000000000000000000000000000000000000000000000000000d5b36b42a700000000000000000000000000000000000000000000000000000011cef39ae300000000000000000000000000000000000000000000000000000093cf4c525e0000000000000000000000000000000000000000000000000000006911d0783d000000000000000000000000000000000000000000000000000000356cdad0a900000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000019d27ad3c9a0000000000000000000000000000000000000000000000000000004573e942760000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000de9ae5101900000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000e7825edd8b0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001396d8c3fa00000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000087580832bf00000000000000000000000000000000000000000000000000000165f2ed42d9000000000000000000000000000000000000000000000000000000d5b36b42a70000000000000000000000000000000000000000000000000000005ad2a72f87000000000000000000000000000000000000000000000000000000d77b506bbe0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001ab66d685400000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000d9433594d5000000000000000000000000000000000000000000000000000000f95152786e000000000000000000000000000000000000000000000000000000de9ae5101900000000000000000000000000000000000000000000000000000050234838fe0000000000000000000000000000000000000000000000000000004e5b630fe8000000000000000000000000000000000000000000000000000000a3d65ac42b000000000000000000000000000000000000000000000000000000c5ac5cd0db00000000000000000000000000000000000000000000000000000063ba20fcf900000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000004c937de6d1000000000000000000000000000000000000000000000000000000602a56aacb00000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000061f23bd3e2000000000000000000000000000000000000000000000000000000356cdad0a900000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000557af7b443000000000000000000000000000000000000000000000000000000eb12292fb8000000000000000000000000000000000000000000000000000000f3f9a2fd2a00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000cb040c4c1f000000000000000000000000000000000000000000000000000000c77441f9f200000000000000000000000000000000000000000000000000000186010a267200000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000006749eb4f26000000000000000000000000000000000000000000000000000000dcd2ffe702000000000000000000000000000000000000000000000000000000cb040c4c1f000000000000000000000000000000000000000000000000000000bcc4e30369000000000000000000000000000000000000000000000000000000658206260f00000000000000000000000000000000000000000000000000000150942f55c800000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000003e54549e1b00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000ce93d69e4d00000000000000000000000000000000000000000000000000000186010a267200000000000000000000000000000000000000000000000000000149749ab16d000000000000000000000000000000000000000000000000000000e5ba79b474000000000000000000000000000000000000000000000000000000658206260f000000000000000000000000000000000000000000000000000000f3f9a2fd2a00000000000000000000000000000000000000000000000000000011cef39ae30000000000000000000000000000000000000000000000000000017b51ab2fea0000000000000000000000000000000000000000000000000000017989c606d3000000000000000000000000000000000000000000000000000001843924fd5b00000000000000000000000000000000000000000000000000000167bad26bf0000000000000000000000000000000000000000000000000000000c93c272308000000000000000000000000000000000000000000000000000000200e1ce3990000000000000000000000000000000000000000000000000000005ad2a72f870000000000000000000000000000000000000000000000000000004573e942760000000000000000000000000000000000000000000000000000008ae7d284ed0000000000000000000000000000000000000000000000000000009e7eab48e700000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000bcc4e3036900000000000000000000000000000000000000000000000000000031dd107e7c0000000000000000000000000000000000000000000000000000009926fbcda3000000000000000000000000000000000000000000000000000000602a56aacb0000000000000000000000000000000000000000000000000000001726a3162700000000000000000000000000000000000000000000000000000050234838fe0000000000000000000000000000000000000000000000000000005ad2a72f870000000000000000000000000000000000000000000000000000008cafb7ae03000000000000000000000000000000000000000000000000000000c93c27230800000000000000000000000000000000000000000000000000000053b3128b2c000000000000000000000000000000000000000000000000000000557af7b44300000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000be8cc82c80000000000000000000000000000000000000000000000000000000fce11cca9c000000000000000000000000000000000000000000000000000000d05bbbc7630000000000000000000000000000000000000000000000000000007031651c980000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000cccbf17536000000000000000000000000000000000000000000000000000000e94a4406a20000000000000000000000000000000000000000000000000000009926fbcda300000000000000000000000000000000000000000000000000000038fca522d70000000000000000000000000000000000000000000000000000004c937de6d100000000000000000000000000000000000000000000000000000043ac04195f0000000000000000000000000000000000000000000000000000008ae7d284ed0000000000000000000000000000000000000000000000000000004c937de6d10000000000000000000000000000000000000000000000000000001ab66d6854000000000000000000000000000000000000000000000000000000602a56aacb000000000000000000000000000000000000000000000000000000e062ca3930000000000000000000000000000000000000000000000000000000de9ae5101900000000000000000000000000000000000000000000000000000033a4f5a7930000000000000000000000000000000000000000000000000000015b438e4c50000000000000000000000000000000000000000000000000000000e062ca393000000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000fce11cca9c000000000000000000000000000000000000000000000000000000c3e477a7c400000000000000000000000000000000000000000000000000000028f596b10a000000000000000000000000000000000000000000000000000000eea1f381e600000000000000000000000000000000000000000000000000000085902309a800000000000000000000000000000000000000000000000000000050234838fe00000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000000272db187f40000000000000000000000000000000000000000000000000000003e54549e1b0000000000000000000000000000000000000000000000000000001726a3162700000000000000000000000000000000000000000000000000000030152b55650000000000000000000000000000000000000000000000000000004acb98bdba00000000000000000000000000000000000000000000000000000043ac04195f0000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000c054ad55970000000000000000000000000000000000000000000000000000003734bff9c00000000000000000000000000000000000000000000000000000017d1990590000000000000000000000000000000000000000000000000000000093cf4c525e000000000000000000000000000000000000000000000000000000356cdad0a900000000000000000000000000000000000000000000000000000050234838fe00000000000000000000000000000000000000000000000000000073c12f6ec500000000000000000000000000000000000000000000000000000051eb2d6215000000000000000000000000000000000000000000000000000000dcd2ffe70200000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000aaf5ef68860000000000000000000000000000000000000000000000000000008e779cd71a0000000000000000000000000000000000000000000000000000008ae7d284ed0000000000000000000000000000000000000000000000000000010238cc45e000000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000009aeee0f6b90000000000000000000000000000000000000000000000000000007750f9c0f30000000000000000000000000000000000000000000000000000009207672948000000000000000000000000000000000000000000000000000000ecda0e58cf000000000000000000000000000000000000000000000000000000d9433594d5000000000000000000000000000000000000000000000000000000f231bdd4130000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000007918deea09000000000000000000000000000000000000000000000000000000e7825edd8b000000000000000000000000000000000000000000000000000000602a56aacb0000000000000000000000000000000000000000000000000000004903b394a40000000000000000000000000000000000000000000000000000006749eb4f26000000000000000000000000000000000000000000000000000000cb040c4c1f00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000590ac2067000000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000d223a0f07a00000000000000000000000000000000000000000000000000000021d6020caf0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001396d8c3fa00000000000000000000000000000000000000000000000000000087580832bf0000000000000000000000000000000000000000000000000000010ce82b3c680000000000000000000000000000000000000000000000000000005c9a8c589e000000000000000000000000000000000000000000000000000000590ac206700000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000fea901f3b200000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000000d223a0f07a0000000000000000000000000000000000000000000000000000018ee883f3e4000000000000000000000000000000000000000000000000000000356cdad0a9000000000000000000000000000000000000000000000000000000eea1f381e60000000000000000000000000000000000000000000000000000008ae7d284ed000000000000000000000000000000000000000000000000000000155ebded10000000000000000000000000000000000000000000000000000000b215840ce1000000000000000000000000000000000000000000000000000000b93518b13c0000000000000000000000000000000000000000000000000000017432168b8f00000000000000000000000000000000000000000000000000000085902309a8000000000000000000000000000000000000000000000000000000473bce6b8d0000000000000000000000000000000000000000000000000000002abd7bda210000000000000000000000000000000000000000000000000000007031651c9800000000000000000000000000000000000000000000000000000053b3128b2c000000000000000000000000000000000000000000000000000000eb12292fb800000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000007031651c9800000000000000000000000000000000000000000000000000000041e41ef049000000000000000000000000000000000000000000000000000000e062ca39300000000000000000000000000000000000000000000000000000005e627181b4000000000000000000000000000000000000000000000000000000d77b506bbe000000000000000000000000000000000000000000000000000000658206260f000000000000000000000000000000000000000000000000000000272db187f4000000000000000000000000000000000000000000000000000000e5ba79b47400000000000000000000000000000000000000000000000000000050234838fe000000000000000000000000000000000000000000000000000000e7825edd8b00000000000000000000000000000000000000000000000000000155ebded10c0000000000000000000000000000000000000000000000000000005742dcdd590000000000000000000000000000000000000000000000000000001ab66d6854000000000000000000000000000000000000000000000000000000155ebded10000000000000000000000000000000000000000000000000000000d9433594d5000000000000000000000000000000000000000000000000000000272db187f4000000000000000000000000000000000000000000000000000000c93c2723080000000000000000000000000000000000000000000000000000006ad9b5a15300000000000000000000000000000000000000000000000000000010070e71cc0000000000000000000000000000000000000000000000000000009597317b75000000000000000000000000000000000000000000000000000000f3f9a2fd2a00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000c77441f9f200000000000000000000000000000000000000000000000000000051eb2d621500000000000000000000000000000000000000000000000000000145e4d05f4000000000000000000000000000000000000000000000000000000010070e71cc00000000000000000000000000000000000000000000000000000083c83de0920000000000000000000000000000000000000000000000000000002565cc5edd00000000000000000000000000000000000000000000000000000083c83de092000000000000000000000000000000000000000000000000000000557af7b44300000000000000000000000000000000000000000000000000000030152b55650000000000000000000000000000000000000000000000000000005c9a8c589e0000000000000000000000000000000000000000000000000000010b20461351000000000000000000000000000000000000000000000000000000cccbf1753600000000000000000000000000000000000000000000000000000165f2ed42d90000000000000000000000000000000000000000000000000000005742dcdd5900000000000000000000000000000000000000000000000000000053b3128b2c000000000000000000000000000000000000000000000000000000155ebded1000000000000000000000000000000000000000000000000000000043ac04195f000000000000000000000000000000000000000000000000000000f95152786e000000000000000000000000000000000000000000000000000000a92e0a3f6f00000000000000000000000000000000000000000000000000000187c8ef4f89000000000000000000000000000000000000000000000000000000155ebded100000000000000000000000000000000000000000000000000000008cafb7ae03000000000000000000000000000000000000000000000000000000bcc4e303690000000000000000000000000000000000000000000000000000002e4d462c4f0000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000dcd2ffe70200000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e00000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000602a56aacb0000000000000000000000000000000000000000000000000000003734bff9c000000000000000000000000000000000000000000000000000000053b3128b2c0000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000005742dcdd5900000000000000000000000000000000000000000000000000000075891497dc000000000000000000000000000000000000000000000000000000c5ac5cd0db0000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000155ebded10000000000000000000000000000000000000000000000000000000de9ae5101900000000000000000000000000000000000000000000000000000165f2ed42d90000000000000000000000000000000000000000000000000000001ab66d68540000000000000000000000000000000000000000000000000000001ab66d68540000000000000000000000000000000000000000000000000000001726a31627000000000000000000000000000000000000000000000000000000401c39c73200000000000000000000000000000000000000000000000000000018ee883f3e000000000000000000000000000000000000000000000000000000155ebded1000000000000000000000000000000000000000000000000000000018ee883f3e0000000000000000000000000000000000000000000000000000007ca8a93c3700000000000000000000000000000000000000000000000000000011cef39ae3000000000000000000000000000000000000000000000000000000be8cc82c800000000000000000000000000000000000000000000000000000006749eb4f260000000000000000000000000000000000000000000000000000001726a316270000000000000000000000000000000000000000000000000000002e4d462c4f000000000000000000000000000000000000000000000000000000d05bbbc7630000000000000000000000000000000000000000000000000000004acb98bdba0000000000000000000000000000000000000000000000000000004acb98bdba0000000000000000000000000000000000000000000000000000001726a31627" + }, + "result": { + "gasUsed": "0x24f678b1", + "output": "0x" + }, + "blockHash": "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber": 3663377, + "transactionHash": "0xdbbc58127cc21ef53ae41e63d815853a748ca6ecee21264466fb8960e0feefe3", + "transactionPosition": 107 + } + ] + """ + + require Logger + + import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] + + alias EthereumJSONRPC.Geth + alias EthereumJSONRPC.Geth.Call + + @behaviour EthereumJSONRPC.Variant + + @doc """ + Block reward contract beneficiary fetching is not supported currently for FEVM. + + To signal to the caller that fetching is not supported, `:ignore` is returned. + """ + @impl EthereumJSONRPC.Variant + def fetch_beneficiaries(_block_range, _json_rpc_named_arguments), do: :ignore + + @doc """ + Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params. + """ + @impl EthereumJSONRPC.Variant + def fetch_internal_transactions(_transactions_params, _json_rpc_named_arguments), do: :ignore + + @doc """ + Fetches the first trace from the trace URL. + """ + @impl EthereumJSONRPC.Variant + def fetch_first_trace(_transactions_params, _json_rpc_named_arguments), do: :ignore + + @doc """ + Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the FEVM `trace_block` URL. + """ + @impl EthereumJSONRPC.Variant + def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) do + id_to_params = id_to_params(block_numbers) + + with {:ok, blocks_responses} <- + id_to_params + |> debug_trace_block_by_number_requests() + |> json_rpc(json_rpc_named_arguments), + :ok <- Geth.check_errors_exist(blocks_responses, id_to_params) do + transactions_params = to_transactions_params(blocks_responses, id_to_params) + + debug_trace_transaction_responses_to_internal_transactions_params(transactions_params) + end + end + + defp to_transactions_params(blocks_responses, id_to_params) do + Enum.reduce(blocks_responses, [], fn %{id: id, result: tx_result}, blocks_acc -> + extract_transactions_params(Map.fetch!(id_to_params, id), tx_result) ++ blocks_acc + end) + end + + defp extract_transactions_params(block_number, tx_result) do + tx_result + |> Enum.reduce( + {[], 0}, + # counter is the index of the internal transaction in transaction + fn %{"transactionHash" => tx_hash, "transactionPosition" => transaction_index} = calls_result, + {tx_acc, counter} -> + last_tx_response_from_accumulator = List.first(tx_acc) + + next_counter = + with {:empty_accumulator, false} <- {:empty_accumulator, is_nil(last_tx_response_from_accumulator)}, + true <- tx_hash !== last_tx_response_from_accumulator["transactionHash"] do + 0 + else + {:empty_accumulator, true} -> + 0 + + _ -> + counter + 1 + end + + { + [ + Map.merge( + %{ + "blockNumber" => block_number, + "transactionHash" => tx_hash, + "transactionIndex" => transaction_index, + "index" => next_counter + }, + calls_result + ) + | tx_acc + ], + next_counter + } + end + ) + |> elem(0) + end + + @doc """ + Fetches the pending transactions from the FEVM node. + """ + @impl EthereumJSONRPC.Variant + def fetch_pending_transactions(_json_rpc_named_arguments), do: :ignore + + defp debug_trace_block_by_number_requests(id_to_params) do + Enum.map(id_to_params, &debug_trace_block_by_number_request/1) + end + + defp debug_trace_block_by_number_request({id, block_number}) do + request(%{ + id: id, + method: "trace_block", + params: [integer_to_quantity(block_number)] + }) + end + + defp debug_trace_transaction_responses_to_internal_transactions_params(responses) + when is_list(responses) do + responses + |> Enum.map(&debug_trace_transaction_response_to_internal_transactions_params(&1)) + |> Geth.reduce_internal_transactions_params() + end + + defp debug_trace_transaction_response_to_internal_transactions_params(call) do + internal_transaction_params = + call + |> parse_trace_block_call() + |> Call.to_internal_transaction_params() + + {:ok, internal_transaction_params} + end + + defp parse_trace_block_call(%{"Type" => type} = call) do + sanitized_call = + call + |> Map.put("type", type) + |> Map.drop(["Type"]) + + parse_trace_block_call(sanitized_call) + end + + defp parse_trace_block_call( + %{"type" => upcase_type, "action" => %{"from" => from} = action, "result" => result} = call + ) do + type = String.downcase(upcase_type) + + %{ + "type" => if(type in ~w(call callcode delegatecall staticcall), do: "call", else: type), + "callType" => type, + "from" => from, + "to" => Map.get(action, "to", "0x"), + "createdContractAddressHash" => Map.get(result, "address", "0x"), + "value" => Map.get(action, "value", "0x0"), + "gas" => Map.get(action, "gas", "0x0"), + "gasUsed" => Map.get(result, "gasUsed", "0x0"), + "input" => Map.get(action, "input", "0x"), + "init" => Map.get(action, "init", "0x"), + "createdContractCode" => Map.get(result, "code", "0x"), + "traceAddress" => Map.get(call, "traceAddress", []), + "blockNumber" => Map.get(call, "blockNumber"), + "index" => Map.get(call, "index"), + "transactionIndex" => Map.get(call, "transactionIndex"), + "transactionHash" => Map.get(call, "transactionHash"), + # : check, that error is returned in the root of the call + "error" => call["error"] + } + |> case do + %{"error" => nil} = ok_call -> + ok_call + |> Map.delete("error") + |> Map.put("output", Map.get(result, "output", "0x")) + + error_call -> + error_call + end + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index 4d864336f50c..fbd067d0d446 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -63,12 +63,71 @@ defmodule EthereumJSONRPC.Geth do def fetch_first_trace(_transactions_params, _json_rpc_named_arguments), do: :ignore @doc """ - Internal transaction fetching for entire blocks is not currently supported for Geth. - - To signal to the caller that fetching is not supported, `:ignore` is returned. + Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Geth trace URL. """ @impl EthereumJSONRPC.Variant - def fetch_block_internal_transactions(_block_range, _json_rpc_named_arguments), do: :ignore + def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) do + id_to_params = id_to_params(block_numbers) + + with {:ok, blocks_responses} <- + id_to_params + |> debug_trace_block_by_number_requests() + |> json_rpc(json_rpc_named_arguments), + :ok <- check_errors_exist(blocks_responses, id_to_params) do + transactions_params = to_transactions_params(blocks_responses, id_to_params) + + {transactions_id_to_params, transactions_responses} = + Enum.reduce(transactions_params, {%{}, []}, fn {params, calls}, {id_to_params_acc, calls_acc} -> + {Map.put(id_to_params_acc, params[:id], params), [calls | calls_acc]} + end) + + debug_trace_transaction_responses_to_internal_transactions_params( + transactions_responses, + transactions_id_to_params, + json_rpc_named_arguments + ) + end + end + + @spec check_errors_exist(list(), %{non_neg_integer() => any()}) :: :ok | {:error, list()} + def check_errors_exist(blocks_responses, id_to_params) do + blocks_responses + |> EthereumJSONRPC.sanitize_responses(id_to_params) + |> Enum.reduce([], fn + %{result: _result}, acc -> acc + %{error: error}, acc -> [error | acc] + end) + |> case do + [] -> :ok + errors -> {:error, errors} + end + end + + defp to_transactions_params(blocks_responses, id_to_params) do + blocks_responses + |> Enum.reduce({[], 0}, fn %{id: id, result: tx_result}, {blocks_acc, counter} -> + {transactions_params, _, new_counter} = + extract_transactions_params(Map.fetch!(id_to_params, id), tx_result, counter) + + {transactions_params ++ blocks_acc, new_counter} + end) + |> elem(0) + end + + defp extract_transactions_params(block_number, tx_result, counter) do + Enum.reduce(tx_result, {[], 0, counter}, fn %{"txHash" => tx_hash, "result" => calls_result}, + {tx_acc, inner_counter, counter} -> + { + [ + {%{block_number: block_number, hash_data: tx_hash, transaction_index: inner_counter, id: counter}, + %{id: counter, result: calls_result}} + | tx_acc + ], + inner_counter + 1, + counter + 1 + } + end) + end @doc """ Fetches the pending transactions from the Geth node. @@ -84,6 +143,10 @@ defmodule EthereumJSONRPC.Geth do end) end + defp debug_trace_block_by_number_requests(id_to_params) do + Enum.map(id_to_params, &debug_trace_block_by_number_request/1) + end + @tracer_path "priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js" @external_resource @tracer_path @tracer File.read!(@tracer_path) @@ -92,30 +155,39 @@ defmodule EthereumJSONRPC.Geth do debug_trace_transaction_timeout = Application.get_env(:ethereum_jsonrpc, __MODULE__)[:debug_trace_transaction_timeout] - tracer = - cond do - tracer_type() == "js" -> - %{"tracer" => @tracer} - - tracer_type() in ~w(opcode polygon_edge) -> - %{ - "enableMemory" => true, - "disableStack" => false, - "disableStorage" => true, - "enableReturnData" => false - } - - true -> - %{"tracer" => "callTracer"} - end - request(%{ id: id, method: "debug_traceTransaction", - params: [hash_data, %{timeout: debug_trace_transaction_timeout} |> Map.merge(tracer)] + params: [hash_data, %{timeout: debug_trace_transaction_timeout} |> Map.merge(tracer_params())] }) end + defp debug_trace_block_by_number_request({id, block_number}) do + request(%{ + id: id, + method: "debug_traceBlockByNumber", + params: [integer_to_quantity(block_number), tracer_params()] + }) + end + + defp tracer_params do + cond do + tracer_type() == "js" -> + %{"tracer" => @tracer} + + tracer_type() in ~w(opcode polygon_edge) -> + %{ + "enableMemory" => true, + "disableStack" => false, + "disableStorage" => true, + "enableReturnData" => false + } + + true -> + %{"tracer" => "callTracer"} + end + end + defp debug_trace_transaction_responses_to_internal_transactions_params( [%{result: %{"structLogs" => _}} | _] = responses, id_to_params, @@ -279,56 +351,53 @@ defmodule EthereumJSONRPC.Geth do defp parse_call_tracer_calls([], acc, _trace_address, _inner?), do: acc defp parse_call_tracer_calls({%{"type" => 0}, _}, acc, _trace_address, _inner?), do: acc - defp parse_call_tracer_calls({%{"type" => "STOP"}, _}, [last | acc], _trace_address, _inner?) do + defp parse_call_tracer_calls({%{"type" => type}, _}, [last | acc], _trace_address, _inner?) + when type in ["STOP", "stop"] do [Map.put(last, "error", "execution stopped") | acc] end - defp parse_call_tracer_calls( - {%{"type" => type, "from" => from} = call, index}, - acc, - trace_address, - inner? - ) - when type in ~w(CALL CALLCODE DELEGATECALL STATICCALL CREATE CREATE2 SELFDESTRUCT REVERT STOP) do - new_trace_address = [index | trace_address] - - formatted_call = - %{ - "type" => if(type in ~w(CALL CALLCODE DELEGATECALL STATICCALL), do: "call", else: String.downcase(type)), - "callType" => String.downcase(type), - "from" => from, - "to" => Map.get(call, "to", "0x"), - "createdContractAddressHash" => Map.get(call, "to", "0x"), - "value" => Map.get(call, "value", "0x0"), - "gas" => Map.get(call, "gas", "0x0"), - "gasUsed" => Map.get(call, "gasUsed", "0x0"), - "input" => Map.get(call, "input", "0x"), - "init" => Map.get(call, "input", "0x"), - "createdContractCode" => Map.get(call, "output", "0x"), - "traceAddress" => if(inner?, do: Enum.reverse(new_trace_address), else: []), - "error" => call["error"] - } - |> case do - %{"error" => nil} = ok_call -> - ok_call - |> Map.delete("error") - # to handle staticcall, all other cases handled by EthereumJSONRPC.Geth.Call.elixir_to_internal_transaction_params/1 - |> Map.put("output", Map.get(call, "output", "0x")) - - error_call -> - error_call - end - - parse_call_tracer_calls( - Map.get(call, "calls", []), - [formatted_call | acc], - if(inner?, do: new_trace_address, else: []) - ) - end + defp parse_call_tracer_calls({%{"type" => upcase_type, "from" => from} = call, index}, acc, trace_address, inner?) do + case String.downcase(upcase_type) do + type when type in ~w(call callcode delegatecall staticcall create create2 selfdestruct revert stop) -> + new_trace_address = [index | trace_address] - defp parse_call_tracer_calls({call, _}, acc, _trace_address, _inner?) do - Logger.warning("Call from a callTracer with an unknown type: #{inspect(call)}") - acc + formatted_call = + %{ + "type" => if(type in ~w(call callcode delegatecall staticcall), do: "call", else: type), + "callType" => type, + "from" => from, + "to" => Map.get(call, "to", "0x"), + "createdContractAddressHash" => Map.get(call, "to", "0x"), + "value" => Map.get(call, "value", "0x0"), + "gas" => Map.get(call, "gas", "0x0"), + "gasUsed" => Map.get(call, "gasUsed", "0x0"), + "input" => Map.get(call, "input", "0x"), + "init" => Map.get(call, "input", "0x"), + "createdContractCode" => Map.get(call, "output", "0x"), + "traceAddress" => if(inner?, do: Enum.reverse(new_trace_address), else: []), + "error" => call["error"] + } + |> case do + %{"error" => nil} = ok_call -> + ok_call + |> Map.delete("error") + # to handle staticcall, all other cases handled by EthereumJSONRPC.Geth.Call.elixir_to_internal_transaction_params/1 + |> Map.put("output", Map.get(call, "output", "0x")) + + error_call -> + error_call + end + + parse_call_tracer_calls( + Map.get(call, "calls", []), + [formatted_call | acc], + if(inner?, do: new_trace_address, else: []) + ) + + _unknown_type -> + Logger.warning("Call from a callTracer with an unknown type: #{inspect(call)}") + acc + end end defp parse_call_tracer_calls(calls, acc, trace_address, _inner) when is_list(calls) do @@ -337,7 +406,8 @@ defmodule EthereumJSONRPC.Geth do |> Enum.reduce(acc, &parse_call_tracer_calls(&1, &2, trace_address)) end - defp reduce_internal_transactions_params(internal_transactions_params) when is_list(internal_transactions_params) do + @spec reduce_internal_transactions_params(list()) :: {:ok, list()} | {:error, list()} + def reduce_internal_transactions_params(internal_transactions_params) when is_list(internal_transactions_params) do internal_transactions_params |> Enum.reduce({:ok, []}, &internal_transactions_params_reducer/2) |> finalize_internal_transactions_params() diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex index e7abd430391b..e1704d1068a8 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.HTTP do def json_rpc(%{method: method} = request, options) when is_map(request) do json = encode_json(request) http = Keyword.fetch!(options, :http) - url = url(options, method) + {url_type, url} = url(options, method) http_options = Keyword.fetch!(options, :http_options) with {:ok, %{body: body, status_code: code}} <- http.json_rpc(url, json, headers(), http_options), @@ -33,7 +33,7 @@ defmodule EthereumJSONRPC.HTTP do else error -> named_arguments = [transport: __MODULE__, transport_options: Keyword.delete(options, :method_to_url)] - EndpointAvailabilityObserver.inc_error_count(url, named_arguments) + EndpointAvailabilityObserver.inc_error_count(url, named_arguments, url_type) error end end @@ -65,7 +65,7 @@ defmodule EthereumJSONRPC.HTTP do defp chunked_json_rpc([[%{method: method} | _] = batch | tail] = chunks, options, decoded_response_bodies) when is_list(tail) and is_list(decoded_response_bodies) do http = Keyword.fetch!(options, :http) - url = url(options, method) + {url_type, url} = url(options, method) http_options = Keyword.fetch!(options, :http_options) json = encode_json(batch) @@ -85,7 +85,7 @@ defmodule EthereumJSONRPC.HTTP do {:error, _} = error -> named_arguments = [transport: __MODULE__, transport_options: Keyword.delete(options, :method_to_url)] - EndpointAvailabilityObserver.inc_error_count(url, named_arguments) + EndpointAvailabilityObserver.inc_error_count(url, named_arguments, url_type) error end end @@ -94,10 +94,21 @@ defmodule EthereumJSONRPC.HTTP do case length(batch) do # it can't be made any smaller 1 -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + Logger.error(fn -> - "413 Request Entity Too Large returned from single request batch. Cannot shrink batch further." + [ + "413 Request Entity Too Large returned from single request batch. Cannot shrink batch further. ", + "The actual batched request was ", + "#{inspect(batch)}. ", + "The actual response of the method was ", + "#{inspect(response)}." + ] end) + Logger.configure(truncate: old_truncate) + {:error, response} batch_size -> @@ -192,12 +203,21 @@ defmodule EthereumJSONRPC.HTTP do with {:ok, method_to_url} <- Keyword.fetch(options, :method_to_url), {:ok, method_atom} <- to_existing_atom(method), {:ok, url} <- Keyword.fetch(method_to_url, method_atom) do - EndpointAvailabilityObserver.maybe_replace_url(url, options[:fallback_trace_url]) + {url_type, fallback_url} = + case method_atom do + :eth_call -> {:eth_call, options[:fallback_eth_call_url]} + _ -> {:trace, options[:fallback_trace_url]} + end + + {url_type, EndpointAvailabilityObserver.maybe_replace_url(url, fallback_url, url_type)} else _ -> - options - |> Keyword.fetch!(:url) - |> EndpointAvailabilityObserver.maybe_replace_url(options[:fallback_url]) + url = + options + |> Keyword.fetch!(:url) + |> EndpointAvailabilityObserver.maybe_replace_url(options[:fallback_url], :http) + + {:http, url} end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex index ecda58364425..012ac0ec01e3 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/log.ex @@ -33,8 +33,7 @@ defmodule EthereumJSONRPC.Log do ...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], ...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", ...> "transactionIndex" => 0, - ...> "transactionLogIndex" => 0, - ...> "type" => "mined" + ...> "transactionLogIndex" => 0 ...> } ...> ) %{ @@ -47,12 +46,9 @@ defmodule EthereumJSONRPC.Log do index: 0, second_topic: nil, third_topic: nil, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } - Geth does not supply a `"type"` - iex> EthereumJSONRPC.Log.elixir_to_params( ...> %{ ...> "address" => "0xda8b3276cde6d768a44b9dac659faa339a41ac55", @@ -82,17 +78,15 @@ defmodule EthereumJSONRPC.Log do } """ - def elixir_to_params( - %{ - "address" => address_hash, - "blockNumber" => block_number, - "blockHash" => block_hash, - "data" => data, - "logIndex" => index, - "topics" => topics, - "transactionHash" => transaction_hash - } = elixir - ) do + def elixir_to_params(%{ + "address" => address_hash, + "blockNumber" => block_number, + "blockHash" => block_hash, + "data" => data, + "logIndex" => index, + "topics" => topics, + "transactionHash" => transaction_hash + }) do %{ address_hash: address_hash, block_number: block_number, @@ -102,7 +96,6 @@ defmodule EthereumJSONRPC.Log do transaction_hash: transaction_hash } |> put_topics(topics) - |> put_type(elixir) end @doc """ @@ -118,8 +111,7 @@ defmodule EthereumJSONRPC.Log do ...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], ...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", ...> "transactionIndex" => "0x0", - ...> "transactionLogIndex" => "0x0", - ...> "type" => "mined" + ...> "transactionLogIndex" => "0x0" ...> } ...> ) %{ @@ -131,8 +123,7 @@ defmodule EthereumJSONRPC.Log do "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => 0, - "transactionLogIndex" => 0, - "type" => "mined" + "transactionLogIndex" => 0 } Geth includes a `"removed"` key @@ -172,7 +163,7 @@ defmodule EthereumJSONRPC.Log do end defp entry_to_elixir({key, _} = entry) - when key in ~w(address blockHash data removed topics transactionHash type timestamp), + when key in ~w(address blockHash data removed topics transactionHash timestamp), do: entry defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber logIndex transactionIndex transactionLogIndex) do @@ -183,6 +174,11 @@ defmodule EthereumJSONRPC.Log do end end + # zkSync specific log fields + defp entry_to_elixir({key, _}) when key in ~w(l1BatchNumber logType) do + {nil, nil} + end + defp put_topics(params, topics) when is_map(params) and is_list(topics) do params |> Map.put(:first_topic, Enum.at(topics, 0)) @@ -190,10 +186,4 @@ defmodule EthereumJSONRPC.Log do |> Map.put(:third_topic, Enum.at(topics, 2)) |> Map.put(:fourth_topic, Enum.at(topics, 3)) end - - defp put_type(params, %{"type" => type}) do - Map.put(params, :type, type) - end - - defp put_type(params, _), do: params end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex index 90de6961f18f..cfce452c8647 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/pending_transaction.ex @@ -60,8 +60,18 @@ defmodule EthereumJSONRPC.PendingTransaction do @spec fetch_pending_transactions_besu(EthereumJSONRPC.json_rpc_named_arguments()) :: {:ok, [Transaction.params()]} | {:error, reason :: term} def fetch_pending_transactions_besu(json_rpc_named_arguments) do + # `txpool_besuPendingTransactions` required parameter `numResults` for number of maximum pending transaction to return. + # + # TODO: Remove fix value when hyperledger besu client change `numResults` from required to optional parameter. + # Current fix value set to `512` bonsai storage default value is 512. + # to handle pending transaction in Ethereum mainnet require more than 100000. + # reference: + # https://etherscan.io/chart/pendingtx + # https://besu.hyperledger.org/public-networks/reference/cli/options#bonsai-historical-block-limit + # + # https://besu.hyperledger.org/public-networks/reference/api#txpool_besupendingtransactions with {:ok, transactions} <- - %{id: 1, method: "txpool_besuTransactions", params: []} + %{id: 1, method: "txpool_besuPendingTransactions", params: [512]} |> request() |> json_rpc(json_rpc_named_arguments) do transactions_params = diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex index 00ededb8d86c..150fd7f18264 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex @@ -74,7 +74,20 @@ defmodule EthereumJSONRPC.Receipt do gas_used: 269607, status: :ok, transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", - transaction_index: 0 + transaction_index: 0,\ + #{case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> """ + blob_gas_price: 0,\ + blob_gas_used: 0\ + """ + "optimism" -> """ + l1_fee: 0,\ + l1_fee_scalar: 0,\ + l1_gas_price: 0,\ + l1_gas_used: 0\ + """ + _ -> "" + end} } Geth, when showing pre-[Byzantium](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-609.md) does not include @@ -107,7 +120,20 @@ defmodule EthereumJSONRPC.Receipt do gas_used: 21001, status: nil, transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", - transaction_index: 0 + transaction_index: 0,\ + #{case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> """ + blob_gas_price: 0,\ + blob_gas_used: 0\ + """ + "optimism" -> """ + l1_fee: 0,\ + l1_fee_scalar: 0,\ + l1_gas_price: 0,\ + l1_gas_used: 0\ + """ + _ -> "" + end} } """ @@ -119,7 +145,13 @@ defmodule EthereumJSONRPC.Receipt do transaction_hash: String.t(), transaction_index: non_neg_integer() } - def elixir_to_params( + def elixir_to_params(elixir) do + elixir + |> do_elixir_to_params() + |> chain_type_fields(elixir) + end + + def do_elixir_to_params( %{ "cumulativeGasUsed" => cumulative_gas_used, "gasUsed" => gas_used, @@ -140,6 +172,29 @@ defmodule EthereumJSONRPC.Receipt do } end + defp chain_type_fields(params, elixir) do + case Application.get_env(:explorer, :chain_type) do + "ethereum" -> + params + |> Map.merge(%{ + blob_gas_price: Map.get(elixir, "blobGasPrice", 0), + blob_gas_used: Map.get(elixir, "blobGasUsed", 0) + }) + + "optimism" -> + params + |> Map.merge(%{ + l1_fee: Map.get(elixir, "l1Fee", 0), + l1_fee_scalar: Map.get(elixir, "l1FeeScalar", 0), + l1_gas_price: Map.get(elixir, "l1GasPrice", 0), + l1_gas_used: Map.get(elixir, "l1GasUsed", 0) + }) + + _ -> + params + end + end + @doc """ Decodes the stringly typed numerical fields to `t:non_neg_integer/0`. @@ -253,11 +308,11 @@ defmodule EthereumJSONRPC.Receipt do # hash format # gas is passed in from the `t:EthereumJSONRPC.Transaction.params/0` to allow pre-Byzantium status to be derived defp entry_to_elixir({key, _} = entry) - when key in ~w(blockHash contractAddress from gas logsBloom root to transactionHash revertReason type effectiveGasPrice), + when key in ~w(blockHash contractAddress from gas logsBloom root to transactionHash revertReason type effectiveGasPrice l1FeeScalar), do: {:ok, entry} defp entry_to_elixir({key, quantity}) - when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex) do + when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex blobGasUsed blobGasPrice l1Fee l1GasPrice l1GasUsed) do result = if is_nil(quantity) do nil @@ -300,7 +355,7 @@ defmodule EthereumJSONRPC.Receipt do end # Arbitrum fields - defp entry_to_elixir({key, _}) when key in ~w(returnData returnCode feeStats l1BlockNumber) do + defp entry_to_elixir({key, _}) when key in ~w(returnData returnCode feeStats l1BlockNumber gasUsedForL1) do :ignore end @@ -315,7 +370,13 @@ defmodule EthereumJSONRPC.Receipt do end # Optimism specific transaction receipt fields - defp entry_to_elixir({key, _}) when key in ~w(depositNonce) do + defp entry_to_elixir({key, _}) when key in ~w(depositNonce depositReceiptVersion) do + :ignore + end + + # zkSync specific transaction receipt fields + defp entry_to_elixir({key, _}) + when key in ~w(l1BatchNumber l1BatchTxIndex l2ToL1Logs) do :ignore end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex index 8e0cffa7f2b1..e897b8502075 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex @@ -32,8 +32,7 @@ defmodule EthereumJSONRPC.Receipts do ...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], ...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", ...> "transactionIndex" => 0, - ...> "transactionLogIndex" => 0, - ...> "type" => "mined" + ...> "transactionLogIndex" => 0 ...> } ...> ], ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", @@ -53,8 +52,7 @@ defmodule EthereumJSONRPC.Receipts do "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => 0, - "transactionLogIndex" => 0, - "type" => "mined" + "transactionLogIndex" => 0 } ] @@ -84,8 +82,7 @@ defmodule EthereumJSONRPC.Receipts do ...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], ...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", ...> "transactionIndex" => 0, - ...> "transactionLogIndex" => 0, - ...> "type" => "mined" + ...> "transactionLogIndex" => 0 ...> } ...> ], ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", @@ -102,7 +99,20 @@ defmodule EthereumJSONRPC.Receipts do gas_used: 50450, status: :ok, transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - transaction_index: 0 + transaction_index: 0,\ + #{case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> """ + blob_gas_price: 0,\ + blob_gas_used: 0\ + """ + "optimism" -> """ + l1_fee: 0,\ + l1_fee_scalar: 0,\ + l1_gas_price: 0,\ + l1_gas_used: 0\ + """ + _ -> "" + end} } ] @@ -165,8 +175,7 @@ defmodule EthereumJSONRPC.Receipts do ...> "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], ...> "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", ...> "transactionIndex" => "0x0", - ...> "transactionLogIndex" => "0x0", - ...> "type" => "mined" + ...> "transactionLogIndex" => "0x0" ...> } ...> ], ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", @@ -193,8 +202,7 @@ defmodule EthereumJSONRPC.Receipts do "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => 0, - "transactionLogIndex" => 0, - "type" => "mined" + "transactionLogIndex" => 0 } ], "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex index a80db36feb5f..77ee7d3906bd 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex @@ -58,7 +58,7 @@ defmodule EthereumJSONRPC.RequestCoordinator do alias EthereumJSONRPC.{RollingWindow, Tracer, Transport} - @error_key :throttleable_error_count + @error_key_base "throttleable_error_count" @throttle_key :throttle_requests_count @doc """ @@ -73,7 +73,9 @@ defmodule EthereumJSONRPC.RequestCoordinator do @spec perform(Transport.batch_request(), Transport.t(), Transport.options(), non_neg_integer()) :: {:ok, Transport.batch_response()} | {:error, term()} def perform(request, transport, transport_options, throttle_timeout) do - sleep_time = sleep_time() + request_method = request_method(request) + + sleep_time = sleep_time(request_method) if sleep_time <= throttle_timeout do :timer.sleep(sleep_time) @@ -85,7 +87,7 @@ defmodule EthereumJSONRPC.RequestCoordinator do trace_request(request, fn -> request |> transport.json_rpc(transport_options) - |> handle_transport_response() + |> handle_transport_response(request_method) end) :error -> @@ -110,19 +112,24 @@ defmodule EthereumJSONRPC.RequestCoordinator do defp trace_request(_, fun), do: fun.() - defp handle_transport_response({:error, {error_type, _}} = error) when error_type in [:bad_gateway, :bad_response] do - RollingWindow.inc(table(), @error_key) + defp request_method([request | _]), do: request_method(request) + defp request_method(%{method: method}), do: method + defp request_method(_), do: nil + + defp handle_transport_response({:error, {error_type, _}} = error, method) + when error_type in [:bad_gateway, :bad_response] do + RollingWindow.inc(table(), method_error_key(method)) inc_throttle_table() error end - defp handle_transport_response({:error, :timeout} = error) do - RollingWindow.inc(table(), @error_key) + defp handle_transport_response({:error, :timeout} = error, method) do + RollingWindow.inc(table(), method_error_key(method)) inc_throttle_table() error end - defp handle_transport_response(response) do + defp handle_transport_response(response, _method) do inc_throttle_table() response end @@ -154,14 +161,16 @@ defmodule EthereumJSONRPC.RequestCoordinator do end end - defp sleep_time do - wait_coefficient = RollingWindow.count(table(), @error_key) + defp sleep_time(request_method) do + wait_coefficient = RollingWindow.count(table(), method_error_key(request_method)) jitter = :rand.uniform(config!(:max_jitter)) wait_per_timeout = config!(:wait_per_timeout) wait_coefficient * (wait_per_timeout + jitter) end + defp method_error_key(method), do: :"#{@error_key_base}_#{method}" + defp table do :rolling_window_opts |> config!() diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index 1e3f8f1cf954..85d3161556ff 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -11,6 +11,47 @@ defmodule EthereumJSONRPC.Transaction do alias EthereumJSONRPC + case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> + @chain_type_fields quote( + do: [ + max_fee_per_blob_gas: non_neg_integer(), + blob_versioned_hashes: [EthereumJSONRPC.hash()] + ] + ) + + "optimism" -> + @chain_type_fields quote( + do: [ + l1_tx_origin: EthereumJSONRPC.hash(), + l1_block_number: non_neg_integer() + ] + ) + + "suave" -> + @chain_type_fields quote( + do: [ + execution_node_hash: EthereumJSONRPC.address(), + wrapped_type: non_neg_integer(), + wrapped_nonce: non_neg_integer(), + wrapped_to_address_hash: EthereumJSONRPC.address(), + wrapped_gas: non_neg_integer(), + wrapped_gas_price: non_neg_integer(), + wrapped_max_priority_fee_per_gas: non_neg_integer(), + wrapped_max_fee_per_gas: non_neg_integer(), + wrapped_value: non_neg_integer(), + wrapped_input: String.t(), + wrapped_v: non_neg_integer(), + wrapped_r: non_neg_integer(), + wrapped_s: non_neg_integer(), + wrapped_hash: EthereumJSONRPC.hash() + ] + ) + + _ -> + @chain_type_fields quote(do: []) + end + @type elixir :: %{ String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.hash() | String.t() | non_neg_integer() | nil } @@ -42,8 +83,21 @@ defmodule EthereumJSONRPC.Transaction do * `"maxPriorityFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max priority fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) * `"maxFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) * `"type"` - `t:EthereumJSONRPC.quantity/0` denotes transaction type. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) - * `"executionNode"` - `t:EthereumJSONRPC.address/0` of execution node (used by Suave). - * `"requestRecord"` - map of wrapped transaction data (used by Suave). + #{case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> """ + * `"maxFeePerBlobGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote max fee per unit of blob gas used. Introduced in [EIP-4844](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md) + * `"blobVersionedHashes"` - `t:list/0` of `t:EthereumJSONRPC.hash/0` of included data blobs hashes. Introduced in [EIP-4844](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-4844.md) + """ + "optimism" -> """ + * `"l1TxOrigin"` - . + * `"l1BlockNumber"` - . + """ + "suave" -> """ + * `"executionNode"` - `t:EthereumJSONRPC.address/0` of execution node (used by Suave). + * `"requestRecord"` - map of wrapped transaction data (used by Suave). + """ + _ -> "" + end} """ @type t :: %{ String.t() => @@ -51,6 +105,7 @@ defmodule EthereumJSONRPC.Transaction do } @type params :: %{ + unquote_splicing(@chain_type_fields), block_hash: EthereumJSONRPC.hash(), block_number: non_neg_integer(), from_address_hash: EthereumJSONRPC.address(), @@ -68,21 +123,7 @@ defmodule EthereumJSONRPC.Transaction do transaction_index: non_neg_integer(), max_priority_fee_per_gas: non_neg_integer(), max_fee_per_gas: non_neg_integer(), - type: non_neg_integer(), - execution_node_hash: EthereumJSONRPC.address(), - wrapped_type: non_neg_integer(), - wrapped_nonce: non_neg_integer(), - wrapped_to_address_hash: EthereumJSONRPC.address(), - wrapped_gas: non_neg_integer(), - wrapped_gas_price: non_neg_integer(), - wrapped_max_priority_fee_per_gas: non_neg_integer(), - wrapped_max_fee_per_gas: non_neg_integer(), - wrapped_value: non_neg_integer(), - wrapped_input: String.t(), - wrapped_v: non_neg_integer(), - wrapped_r: non_neg_integer(), - wrapped_s: non_neg_integer(), - wrapped_hash: EthereumJSONRPC.hash() + type: non_neg_integer() } @doc """ @@ -102,6 +143,7 @@ defmodule EthereumJSONRPC.Transaction do ...> "s" => 31606574786494953692291101914709926755545765281581808821704454381804773090106, ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", ...> "transactionIndex" => 0, + ...> "type" => 2, ...> "v" => 28, ...> "value" => 31337 ...> } @@ -119,11 +161,49 @@ defmodule EthereumJSONRPC.Transaction do r: 61965845294689009770156372156374760022787886965323743865986648153755601564112, s: 31606574786494953692291101914709926755545765281581808821704454381804773090106, to_address_hash: "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + type: 2, v: 28, value: 31337, transaction_index: 0 } + iex> EthereumJSONRPC.Transaction.elixir_to_params( + ...> %{ + ...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + ...> "blockNumber" => 46147, + ...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4", + ...> "gas" => 21000, + ...> "hash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + ...> "input" => "0x", + ...> "nonce" => 0, + ...> "r" => 61965845294689009770156372156374760022787886965323743865986648153755601564112, + ...> "s" => 31606574786494953692291101914709926755545765281581808821704454381804773090106, + ...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + ...> "transactionIndex" => 0, + ...> "type" => 2, + ...> "v" => 28, + ...> "value" => 31337 + ...> } + ...> ) + %{ + block_hash: "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd", + block_number: 46147, + from_address_hash: "0xa1e4380a3b1f749673e270229993ee55f35663b4", + gas: 21000, + hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060", + index: 0, + input: "0x", + nonce: 0, + r: 61965845294689009770156372156374760022787886965323743865986648153755601564112, + s: 31606574786494953692291101914709926755545765281581808821704454381804773090106, + to_address_hash: "0x5df9b87991262f6ba471f09758cde1c0fc1de734", + type: 2, + v: 28, + value: 31337, + transaction_index: 0, + gas_price: 0 + } + Erigon `elixir` from txpool_content method can be converted to `params`. iex> EthereumJSONRPC.Transaction.elixir_to_params( @@ -168,83 +248,13 @@ defmodule EthereumJSONRPC.Transaction do } """ @spec elixir_to_params(elixir) :: params - - # this is for Suave chain (handles `executionNode` and `requestRecord` fields along with EIP-1559 fields) - def elixir_to_params( - %{ - "blockHash" => block_hash, - "blockNumber" => block_number, - "from" => from_address_hash, - "gas" => gas, - "gasPrice" => gas_price, - "hash" => hash, - "input" => input, - "nonce" => nonce, - "r" => r, - "s" => s, - "to" => to_address_hash, - "transactionIndex" => index, - "v" => v, - "value" => value, - "type" => type, - "maxPriorityFeePerGas" => max_priority_fee_per_gas, - "maxFeePerGas" => max_fee_per_gas, - "executionNode" => execution_node_hash, - "requestRecord" => wrapped - } = transaction - ) do - result = %{ - block_hash: block_hash, - block_number: block_number, - from_address_hash: from_address_hash, - gas: gas, - gas_price: gas_price, - hash: hash, - index: index, - input: input, - nonce: nonce, - r: r, - s: s, - to_address_hash: to_address_hash, - v: v, - value: value, - transaction_index: index, - type: type, - max_priority_fee_per_gas: max_priority_fee_per_gas, - max_fee_per_gas: max_fee_per_gas - } - - # credo:disable-for-next-line - result = - if Application.get_env(:explorer, :chain_type) == "suave" do - Map.merge(result, %{ - execution_node_hash: execution_node_hash, - wrapped_type: quantity_to_integer(Map.get(wrapped, "type")), - wrapped_nonce: quantity_to_integer(Map.get(wrapped, "nonce")), - wrapped_to_address_hash: Map.get(wrapped, "to"), - wrapped_gas: quantity_to_integer(Map.get(wrapped, "gas")), - wrapped_gas_price: quantity_to_integer(Map.get(wrapped, "gasPrice")), - wrapped_max_priority_fee_per_gas: quantity_to_integer(Map.get(wrapped, "maxPriorityFeePerGas")), - wrapped_max_fee_per_gas: quantity_to_integer(Map.get(wrapped, "maxFeePerGas")), - wrapped_value: quantity_to_integer(Map.get(wrapped, "value")), - wrapped_input: Map.get(wrapped, "input"), - wrapped_v: quantity_to_integer(Map.get(wrapped, "v")), - wrapped_r: quantity_to_integer(Map.get(wrapped, "r")), - wrapped_s: quantity_to_integer(Map.get(wrapped, "s")), - wrapped_hash: Map.get(wrapped, "hash") - }) - else - result - end - - if transaction["creates"] do - Map.put(result, :created_contract_address_hash, transaction["creates"]) - else - result - end + def elixir_to_params(elixir) do + elixir + |> do_elixir_to_params() + |> chain_type_fields(elixir) end - def elixir_to_params( + def do_elixir_to_params( %{ "blockHash" => block_hash, "blockNumber" => block_number, @@ -254,11 +264,8 @@ defmodule EthereumJSONRPC.Transaction do "hash" => hash, "input" => input, "nonce" => nonce, - "r" => r, - "s" => s, "to" => to_address_hash, "transactionIndex" => index, - "v" => v, "value" => value, "type" => type, "maxPriorityFeePerGas" => max_priority_fee_per_gas, @@ -275,10 +282,7 @@ defmodule EthereumJSONRPC.Transaction do index: index, input: input, nonce: nonce, - r: r, - s: s, to_address_hash: to_address_hash, - v: v, value: value, transaction_index: index, type: type, @@ -286,16 +290,18 @@ defmodule EthereumJSONRPC.Transaction do max_fee_per_gas: max_fee_per_gas } - if transaction["creates"] do - Map.put(result, :created_contract_address_hash, transaction["creates"]) - else - result - end + put_if_present(transaction, result, [ + {"creates", :created_contract_address_hash}, + {"block_timestamp", :block_timestamp}, + {"r", :r}, + {"s", :s}, + {"v", :v} + ]) end # txpool_content method on Erigon node returns tx data # without gas price - def elixir_to_params( + def do_elixir_to_params( %{ "blockHash" => block_hash, "blockNumber" => block_number, @@ -304,11 +310,8 @@ defmodule EthereumJSONRPC.Transaction do "hash" => hash, "input" => input, "nonce" => nonce, - "r" => r, - "s" => s, "to" => to_address_hash, "transactionIndex" => index, - "v" => v, "value" => value, "type" => type, "maxPriorityFeePerGas" => max_priority_fee_per_gas, @@ -325,10 +328,7 @@ defmodule EthereumJSONRPC.Transaction do index: index, input: input, nonce: nonce, - r: r, - s: s, to_address_hash: to_address_hash, - v: v, value: value, transaction_index: index, type: type, @@ -336,15 +336,17 @@ defmodule EthereumJSONRPC.Transaction do max_fee_per_gas: max_fee_per_gas } - if transaction["creates"] do - Map.put(result, :created_contract_address_hash, transaction["creates"]) - else - result - end + put_if_present(transaction, result, [ + {"creates", :created_contract_address_hash}, + {"block_timestamp", :block_timestamp}, + {"r", :r}, + {"s", :s}, + {"v", :v} + ]) end - # this is for Suave chain (handles `executionNode` and `requestRecord` fields without EIP-1559 fields) - def elixir_to_params( + # for legacy txs without maxPriorityFeePerGas and maxFeePerGas + def do_elixir_to_params( %{ "blockHash" => block_hash, "blockNumber" => block_number, @@ -354,15 +356,10 @@ defmodule EthereumJSONRPC.Transaction do "hash" => hash, "input" => input, "nonce" => nonce, - "r" => r, - "s" => s, "to" => to_address_hash, "transactionIndex" => index, - "v" => v, "value" => value, - "type" => type, - "executionNode" => execution_node_hash, - "requestRecord" => wrapped + "type" => type } = transaction ) do result = %{ @@ -375,46 +372,23 @@ defmodule EthereumJSONRPC.Transaction do index: index, input: input, nonce: nonce, - r: r, - s: s, to_address_hash: to_address_hash, - v: v, value: value, transaction_index: index, type: type } - # credo:disable-for-next-line - result = - if Application.get_env(:explorer, :chain_type) == "suave" do - Map.merge(result, %{ - execution_node_hash: execution_node_hash, - wrapped_type: quantity_to_integer(Map.get(wrapped, "type")), - wrapped_nonce: quantity_to_integer(Map.get(wrapped, "nonce")), - wrapped_to_address_hash: Map.get(wrapped, "to"), - wrapped_gas: quantity_to_integer(Map.get(wrapped, "gas")), - wrapped_gas_price: quantity_to_integer(Map.get(wrapped, "gasPrice")), - wrapped_max_priority_fee_per_gas: quantity_to_integer(Map.get(wrapped, "maxPriorityFeePerGas")), - wrapped_max_fee_per_gas: quantity_to_integer(Map.get(wrapped, "maxFeePerGas")), - wrapped_value: quantity_to_integer(Map.get(wrapped, "value")), - wrapped_input: Map.get(wrapped, "input"), - wrapped_v: quantity_to_integer(Map.get(wrapped, "v")), - wrapped_r: quantity_to_integer(Map.get(wrapped, "r")), - wrapped_s: quantity_to_integer(Map.get(wrapped, "s")), - wrapped_hash: Map.get(wrapped, "hash") - }) - else - result - end - - if transaction["creates"] do - Map.put(result, :created_contract_address_hash, transaction["creates"]) - else - result - end + put_if_present(transaction, result, [ + {"creates", :created_contract_address_hash}, + {"block_timestamp", :block_timestamp}, + {"r", :r}, + {"s", :s}, + {"v", :v} + ]) end - def elixir_to_params( + # for legacy txs without type, maxPriorityFeePerGas and maxFeePerGas + def do_elixir_to_params( %{ "blockHash" => block_hash, "blockNumber" => block_number, @@ -424,13 +398,9 @@ defmodule EthereumJSONRPC.Transaction do "hash" => hash, "input" => input, "nonce" => nonce, - "r" => r, - "s" => s, "to" => to_address_hash, "transactionIndex" => index, - "v" => v, - "value" => value, - "type" => type + "value" => value } = transaction ) do result = %{ @@ -443,37 +413,33 @@ defmodule EthereumJSONRPC.Transaction do index: index, input: input, nonce: nonce, - r: r, - s: s, to_address_hash: to_address_hash, - v: v, value: value, - transaction_index: index, - type: type + transaction_index: index } - if transaction["creates"] do - Map.put(result, :created_contract_address_hash, transaction["creates"]) - else - result - end + put_if_present(transaction, result, [ + {"creates", :created_contract_address_hash}, + {"block_timestamp", :block_timestamp}, + {"r", :r}, + {"s", :s}, + {"v", :v} + ]) end - def elixir_to_params( + # for txs without gasPrice, maxPriorityFeePerGas and maxFeePerGas + def do_elixir_to_params( %{ "blockHash" => block_hash, "blockNumber" => block_number, "from" => from_address_hash, "gas" => gas, - "gasPrice" => gas_price, "hash" => hash, "input" => input, "nonce" => nonce, - "r" => r, - "s" => s, "to" => to_address_hash, "transactionIndex" => index, - "v" => v, + "type" => type, "value" => value } = transaction ) do @@ -482,23 +448,69 @@ defmodule EthereumJSONRPC.Transaction do block_number: block_number, from_address_hash: from_address_hash, gas: gas, - gas_price: gas_price, + gas_price: 0, hash: hash, index: index, input: input, nonce: nonce, - r: r, - s: s, to_address_hash: to_address_hash, - v: v, value: value, - transaction_index: index + transaction_index: index, + type: type } - if transaction["creates"] do - Map.put(result, :created_contract_address_hash, transaction["creates"]) - else - result + put_if_present(transaction, result, [ + {"creates", :created_contract_address_hash}, + {"block_timestamp", :block_timestamp}, + {"r", :r}, + {"s", :s}, + {"v", :v} + ]) + end + + defp chain_type_fields(params, elixir) do + case Application.get_env(:explorer, :chain_type) do + "ethereum" -> + put_if_present(elixir, params, [ + {"blobVersionedHashes", :blob_versioned_hashes}, + {"maxFeePerBlobGas", :max_fee_per_blob_gas} + ]) + + "optimism" -> + # we need to put blobVersionedHashes for Indexer.Fetcher.Optimism.TxnBatch module + put_if_present(elixir, params, [ + {"l1TxOrigin", :l1_tx_origin}, + {"l1BlockNumber", :l1_block_number}, + {"blobVersionedHashes", :blob_versioned_hashes} + ]) + + "suave" -> + wrapped = Map.get(elixir, "requestRecord") + + if is_nil(wrapped) do + params + else + params + |> Map.merge(%{ + execution_node_hash: Map.get(elixir, "executionNode"), + wrapped_type: quantity_to_integer(Map.get(wrapped, "type")), + wrapped_nonce: quantity_to_integer(Map.get(wrapped, "nonce")), + wrapped_to_address_hash: Map.get(wrapped, "to"), + wrapped_gas: quantity_to_integer(Map.get(wrapped, "gas")), + wrapped_gas_price: quantity_to_integer(Map.get(wrapped, "gasPrice")), + wrapped_max_priority_fee_per_gas: quantity_to_integer(Map.get(wrapped, "maxPriorityFeePerGas")), + wrapped_max_fee_per_gas: quantity_to_integer(Map.get(wrapped, "maxFeePerGas")), + wrapped_value: quantity_to_integer(Map.get(wrapped, "value")), + wrapped_input: Map.get(wrapped, "input"), + wrapped_v: quantity_to_integer(Map.get(wrapped, "v")), + wrapped_r: quantity_to_integer(Map.get(wrapped, "r")), + wrapped_s: quantity_to_integer(Map.get(wrapped, "s")), + wrapped_hash: Map.get(wrapped, "hash") + }) + end + + _ -> + params end end @@ -580,11 +592,14 @@ defmodule EthereumJSONRPC.Transaction do } """ - def to_elixir(transaction) when is_map(transaction) do - Enum.into(transaction, %{}, &entry_to_elixir/1) + def to_elixir(transaction, block_timestamp \\ nil) + + def to_elixir(transaction, block_timestamp) when is_map(transaction) do + initial = (block_timestamp && %{"block_timestamp" => block_timestamp}) || %{} + Enum.into(transaction, initial, &entry_to_elixir/1) end - def to_elixir(transaction) when is_binary(transaction) do + def to_elixir(transaction, _block_timestamp) when is_binary(transaction) do nil end @@ -608,7 +623,7 @@ defmodule EthereumJSONRPC.Transaction do # # "txType": to avoid FunctionClauseError when indexing Wanchain defp entry_to_elixir({key, value}) - when key in ~w(blockHash condition creates from hash input jsonrpc publicKey raw to txType executionNode requestRecord), + when key in ~w(blockHash condition creates from hash input jsonrpc publicKey raw to txType executionNode requestRecord blobVersionedHashes), do: {key, value} # specific to Nethermind client @@ -616,7 +631,7 @@ defmodule EthereumJSONRPC.Transaction do do: {"input", value} defp entry_to_elixir({key, quantity}) - when key in ~w(gas gasPrice nonce r s standardV v value type maxPriorityFeePerGas maxFeePerGas) and + when key in ~w(gas gasPrice nonce r s standardV v value type maxPriorityFeePerGas maxFeePerGas maxFeePerBlobGas) and quantity != nil do {key, quantity_to_integer(quantity)} end @@ -627,7 +642,8 @@ defmodule EthereumJSONRPC.Transaction do end # quantity or nil for pending - defp entry_to_elixir({key, quantity_or_nil}) when key in ~w(blockNumber transactionIndex) do + defp entry_to_elixir({key, quantity_or_nil}) + when key in ~w(blockNumber transactionIndex l1TxOrigin l1BlockNumber) do elixir = case quantity_or_nil do nil -> nil @@ -645,7 +661,24 @@ defmodule EthereumJSONRPC.Transaction do end end + # ZkSync fields + defp entry_to_elixir({key, _}) when key in ~w(l1BatchNumber l1BatchTxIndex) do + {:ignore, :ignore} + end + defp entry_to_elixir(_) do {nil, nil} end + + defp put_if_present(transaction, result, keys) do + Enum.reduce(keys, result, fn {from_key, to_key}, acc -> + value = transaction[from_key] + + if value do + Map.put(acc, to_key, value) + else + acc + end + end) + end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex index 9b3937873932..ecdf103b4e89 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex @@ -151,9 +151,9 @@ defmodule EthereumJSONRPC.Transactions do ] """ - def to_elixir(transactions) when is_list(transactions) do + def to_elixir(transactions, block_timestamp \\ nil) when is_list(transactions) do transactions - |> Enum.map(&Transaction.to_elixir/1) + |> Enum.map(&Transaction.to_elixir(&1, block_timestamp)) |> Enum.filter(&(!is_nil(&1))) end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transport.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transport.ex index dc7f1839a6b7..829f5816d3f8 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transport.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transport.ex @@ -84,7 +84,7 @@ defmodule EthereumJSONRPC.Transport do * `{:ok, result}` - `result` is the `/result` from JSONRPC response object of format `%{"id" => ..., "result" => result}`. - * `{:error, reason}` - `reason` is the the `/error` from JSONRPC response object of format + * `{:error, reason}` - `reason` is the `/error` from JSONRPC response object of format `%{"id" => ..., "error" => reason}`. The transport can also give any `term()` for `reason` if a more specific reason is possible. diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_checker.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_checker.ex index 527f8a0c35f7..45f2f687d867 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_checker.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_checker.ex @@ -5,6 +5,8 @@ defmodule EthereumJSONRPC.Utility.EndpointAvailabilityChecker do use GenServer + require Logger + alias EthereumJSONRPC.Utility.EndpointAvailabilityObserver @check_interval :timer.seconds(1) @@ -19,24 +21,27 @@ defmodule EthereumJSONRPC.Utility.EndpointAvailabilityChecker do {:ok, %{unavailable_endpoints_arguments: []}} end - def add_endpoint(json_rpc_named_arguments) do - GenServer.cast(__MODULE__, {:add_endpoint, json_rpc_named_arguments}) + def add_endpoint(json_rpc_named_arguments, url_type) do + GenServer.cast(__MODULE__, {:add_endpoint, json_rpc_named_arguments, url_type}) end - def handle_cast({:add_endpoint, named_arguments}, %{unavailable_endpoints_arguments: unavailable} = state) do - {:noreply, %{state | unavailable_endpoints_arguments: [named_arguments | unavailable]}} + def handle_cast({:add_endpoint, named_arguments, url_type}, %{unavailable_endpoints_arguments: unavailable} = state) do + {:noreply, %{state | unavailable_endpoints_arguments: [{named_arguments, url_type} | unavailable]}} end def handle_info(:check, %{unavailable_endpoints_arguments: unavailable_endpoints_arguments} = state) do new_unavailable_endpoints = - Enum.reduce(unavailable_endpoints_arguments, [], fn json_rpc_named_arguments, acc -> + Enum.reduce(unavailable_endpoints_arguments, [], fn {json_rpc_named_arguments, url_type}, acc -> case fetch_latest_block_number(json_rpc_named_arguments) do {:ok, _number} -> - EndpointAvailabilityObserver.enable_endpoint(json_rpc_named_arguments[:transport_options][:url]) + url = json_rpc_named_arguments[:transport_options][:url] + + EndpointAvailabilityObserver.enable_endpoint(url, url_type) + log_url_available(url, url_type, json_rpc_named_arguments) acc _ -> - [json_rpc_named_arguments | acc] + [{json_rpc_named_arguments, url_type} | acc] end end) @@ -45,6 +50,15 @@ defmodule EthereumJSONRPC.Utility.EndpointAvailabilityChecker do {:noreply, %{state | unavailable_endpoints_arguments: new_unavailable_endpoints}} end + defp log_url_available(url, url_type, json_rpc_named_arguments) do + message_extra = + if EndpointAvailabilityObserver.fallback_url_set?(url_type, json_rpc_named_arguments), + do: ", switching back to it", + else: "" + + Logger.info("URL #{inspect(url)} is available now#{message_extra}") + end + defp fetch_latest_block_number(json_rpc_named_arguments) do {_, arguments_without_fallback} = pop_in(json_rpc_named_arguments, [:transport_options, :fallback_url]) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_observer.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_observer.ex index ae4d2d1f2bbe..fe768190b144 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_observer.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/endpoint_availability_observer.ex @@ -5,6 +5,8 @@ defmodule EthereumJSONRPC.Utility.EndpointAvailabilityObserver do use GenServer + require Logger + alias EthereumJSONRPC.Utility.EndpointAvailabilityChecker @max_error_count 3 @@ -18,72 +20,118 @@ defmodule EthereumJSONRPC.Utility.EndpointAvailabilityObserver do def init(_) do schedule_next_cleaning() - {:ok, %{error_counts: %{}, unavailable_endpoints: []}} + {:ok, %{error_counts: %{}, unavailable_endpoints: %{ws: [], trace: [], http: [], eth_call: []}}} end - def inc_error_count(url, json_rpc_named_arguments) do - GenServer.cast(__MODULE__, {:inc_error_count, url, json_rpc_named_arguments}) + def inc_error_count(url, json_rpc_named_arguments, url_type) do + GenServer.cast(__MODULE__, {:inc_error_count, url, json_rpc_named_arguments, url_type}) end - def check_endpoint(url) do - GenServer.call(__MODULE__, {:check_endpoint, url}) + def check_endpoint(url, url_type) do + GenServer.call(__MODULE__, {:check_endpoint, url, url_type}) end - def maybe_replace_url(url, replace_url) do - case check_endpoint(url) do + def maybe_replace_url(url, replace_url, url_type) do + case check_endpoint(url, url_type) do :ok -> url :unavailable -> replace_url || url end end - def enable_endpoint(url) do - GenServer.cast(__MODULE__, {:enable_endpoint, url}) + def enable_endpoint(url, url_type) do + GenServer.cast(__MODULE__, {:enable_endpoint, url, url_type}) end - def handle_call({:check_endpoint, url}, _from, %{unavailable_endpoints: unavailable_endpoints} = state) do - result = if url in unavailable_endpoints, do: :unavailable, else: :ok + def handle_call({:check_endpoint, url, url_type}, _from, %{unavailable_endpoints: unavailable_endpoints} = state) do + result = if url in unavailable_endpoints[url_type], do: :unavailable, else: :ok {:reply, result, state} end - def handle_cast({:inc_error_count, url, json_rpc_named_arguments}, %{error_counts: error_counts} = state) do - current_count = error_counts[url][:count] - unavailable_endpoints = state.unavailable_endpoints - + def handle_cast({:inc_error_count, url, json_rpc_named_arguments, url_type}, state) do new_state = - cond do - url in unavailable_endpoints -> - state - - is_nil(current_count) -> - %{state | error_counts: Map.put(error_counts, url, %{count: 1, last_occasion: now()})} - - current_count + 1 >= @max_error_count -> - EndpointAvailabilityChecker.add_endpoint(put_in(json_rpc_named_arguments[:transport_options][:url], url)) - %{state | error_counts: Map.delete(error_counts, url), unavailable_endpoints: [url | unavailable_endpoints]} - - true -> - %{state | error_counts: Map.put(error_counts, url, %{count: current_count + 1, last_occasion: now()})} - end + if json_rpc_named_arguments[:api?], + do: state, + else: do_increase_error_counts(url, json_rpc_named_arguments, url_type, state) {:noreply, new_state} end - def handle_cast({:enable_endpoint, url}, %{unavailable_endpoints: unavailable_endpoints} = state) do - {:noreply, %{state | unavailable_endpoints: unavailable_endpoints -- [url]}} + def handle_cast({:enable_endpoint, url, url_type}, %{unavailable_endpoints: unavailable_endpoints} = state) do + {:noreply, + %{state | unavailable_endpoints: %{unavailable_endpoints | url_type => unavailable_endpoints[url_type] -- [url]}}} end def handle_info(:clear_old_records, %{error_counts: error_counts} = state) do - new_error_counts = - Enum.reduce(error_counts, %{}, fn {url, %{last_occasion: last_occasion} = record}, acc -> - if now() - last_occasion > @window_duration, do: acc, else: Map.put(acc, url, record) - end) + new_error_counts = Enum.reduce(error_counts, %{}, &do_clear_old_records/2) schedule_next_cleaning() {:noreply, %{state | error_counts: new_error_counts}} end + defp do_clear_old_records({url, counts_by_types}, acc) do + counts_by_types + |> Enum.reduce(%{}, fn {type, %{last_occasion: last_occasion} = record}, acc -> + if now() - last_occasion > @window_duration, do: acc, else: Map.put(acc, type, record) + end) + |> case do + empty_map when empty_map == %{} -> acc + non_empty_map -> Map.put(acc, url, non_empty_map) + end + end + + defp do_increase_error_counts(url, json_rpc_named_arguments, url_type, %{error_counts: error_counts} = state) do + current_count = error_counts[url][url_type][:count] + unavailable_endpoints = state.unavailable_endpoints[url_type] + + cond do + url in unavailable_endpoints -> + state + + is_nil(current_count) -> + %{state | error_counts: Map.put(error_counts, url, %{url_type => %{count: 1, last_occasion: now()}})} + + current_count + 1 >= @max_error_count -> + EndpointAvailabilityChecker.add_endpoint( + put_in(json_rpc_named_arguments[:transport_options][:url], url), + url_type + ) + + log_url_unavailable(url, url_type, json_rpc_named_arguments) + + %{ + state + | error_counts: Map.put(error_counts, url, Map.delete(error_counts[url], url_type)), + unavailable_endpoints: %{state.unavailable_endpoints | url_type => [url | unavailable_endpoints]} + } + + true -> + %{ + state + | error_counts: Map.put(error_counts, url, %{url_type => %{count: current_count + 1, last_occasion: now()}}) + } + end + end + + defp log_url_unavailable(url, url_type, json_rpc_named_arguments) do + fallback_url_message = + if fallback_url_set?(url_type, json_rpc_named_arguments), + do: "switching to fallback #{url_type} url", + else: "and no fallback is set" + + Logger.warning("URL #{inspect(url)} is unavailable, #{fallback_url_message}") + end + + def fallback_url_set?(url_type, json_rpc_named_arguments) do + case url_type do + :http -> not is_nil(json_rpc_named_arguments[:transport_options][:fallback_url]) + :trace -> not is_nil(json_rpc_named_arguments[:transport_options][:fallback_trace_url]) + :eth_call -> not is_nil(json_rpc_named_arguments[:transport_options][:fallback_eth_call_url]) + _ -> false + end + end + defp schedule_next_cleaning do Process.send_after(self(), :clear_old_records, @cleaning_interval) end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex new file mode 100644 index 000000000000..f7220b044d0b --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex @@ -0,0 +1,116 @@ +# credo:disable-for-this-file +defmodule EthereumJSONRPC.Utility.RangesHelper do + @moduledoc """ + Helper for ranges manipulations. + """ + + @default_trace_block_ranges "0..latest" + + @spec traceable_block_number?(integer() | nil) :: boolean() + def traceable_block_number?(block_number) do + if trace_ranges_present?() do + number_in_ranges?(block_number, get_trace_block_ranges()) + else + true + end + end + + @spec filter_traceable_block_numbers([integer()]) :: [integer()] + def filter_traceable_block_numbers(block_numbers) do + if trace_ranges_present?() do + trace_block_ranges = get_trace_block_ranges() + Enum.filter(block_numbers, &number_in_ranges?(&1, trace_block_ranges)) + else + block_numbers + end + end + + @spec trace_ranges_present? :: boolean() + def trace_ranges_present? do + Application.get_env(:indexer, :trace_block_ranges) != @default_trace_block_ranges + end + + @spec get_trace_block_ranges :: [Range.t() | integer()] + def get_trace_block_ranges do + :indexer + |> Application.get_env(:trace_block_ranges) + |> parse_block_ranges() + end + + @spec parse_block_ranges(binary()) :: [Range.t() | integer()] + def parse_block_ranges(block_ranges_string) do + block_ranges_string + |> String.split(",") + |> Enum.map(fn string_range -> + case String.split(string_range, "..") do + [from_string, "latest"] -> + parse_integer(from_string) + + [from_string, to_string] -> + get_from_to(from_string, to_string) + + _ -> + nil + end + end) + |> sanitize_ranges() + end + + defp number_in_ranges?(number, ranges) do + Enum.reduce_while(ranges, false, fn + _from.._to = range, _acc -> if number in range, do: {:halt, true}, else: {:cont, false} + num_to_latest, _acc -> if number >= num_to_latest, do: {:halt, true}, else: {:cont, false} + end) + end + + defp get_from_to(from_string, to_string) do + with {from, ""} <- Integer.parse(from_string), + {to, ""} <- Integer.parse(to_string) do + if from <= to, do: from..to, else: nil + else + _ -> nil + end + end + + @spec sanitize_ranges([Range.t() | integer()]) :: [Range.t() | integer()] + def sanitize_ranges(ranges) do + ranges + |> Enum.reject(&is_nil/1) + |> Enum.sort_by( + fn + from.._to -> from + el -> el + end, + :asc + ) + |> Enum.chunk_while( + nil, + fn + _from.._to = chunk, nil -> + {:cont, chunk} + + _ch_from..ch_to = chunk, acc_from..acc_to = acc -> + if Range.disjoint?(chunk, acc), + do: {:cont, acc, chunk}, + else: {:cont, acc_from..max(ch_to, acc_to)} + + num, nil -> + {:halt, num} + + num, acc_from.._ = acc -> + if Range.disjoint?(num..num, acc), do: {:cont, acc, num}, else: {:halt, acc_from} + + _, num -> + {:halt, num} + end, + fn remainder -> {:cont, remainder, nil} end + ) + end + + defp parse_integer(string) do + case Integer.parse(string) do + {number, ""} -> number + _ -> nil + end + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex index 2468636538a5..bfdf5c624f3b 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/variant.ex @@ -96,7 +96,9 @@ defmodule EthereumJSONRPC.Variant do ) :: {:ok, [raw_trace_params]} | {:error, reason :: term} | :ignore def get do - variant = System.get_env("ETHEREUM_JSONRPC_VARIANT", "nethermind") + default_variant = get_default_variant() + + variant = System.get_env("ETHEREUM_JSONRPC_VARIANT", default_variant) cond do is_nil(variant) -> @@ -112,4 +114,19 @@ defmodule EthereumJSONRPC.Variant do |> String.downcase() end end + + # credo:disable-for-next-line + defp get_default_variant do + case Application.get_env(:explorer, :chain_type) do + "optimism" -> "geth" + "polygon_zkevm" -> "geth" + "zetachain" -> "geth" + "shibarium" -> "geth" + "stability" -> "geth" + "zksync" -> "geth" + "rsk" -> "rsk" + "filecoin" -> "filecoin" + _ -> "nethermind" + end + end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket.ex index 43c890603290..cb89d0bb05fb 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket.ex @@ -53,7 +53,7 @@ defmodule EthereumJSONRPC.WebSocket do * `{:ok, result}` - `result` is the `/result` from JSONRPC response object of format `%{"id" => ..., "result" => result}`. - * `{:error, reason}` - `reason` is the the `/error` from JSONRPC response object of format + * `{:error, reason}` - `reason` is the `/error` from JSONRPC response object of format `%{"id" => ..., "error" => reason}`. The transport can also give any `term()` for `reason` if a more specific reason is possible. diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 9159a8c9852b..63d06afe77c5 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -11,7 +11,7 @@ defmodule EthereumJsonrpc.MixProject do deps_path: "../../deps", description: "Ethereum JSONRPC client.", dialyzer: [ - plt_add_deps: :transitive, + plt_add_deps: :app_tree, plt_add_apps: [:mix], ignore_warnings: "../../.dialyzer-ignore" ], @@ -23,7 +23,7 @@ defmodule EthereumJsonrpc.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "5.3.1" + version: "6.3.0" ] end @@ -85,7 +85,8 @@ defmodule EthereumJsonrpc.MixProject do {:decimal, "~> 2.0"}, {:decorator, "~> 1.4"}, {:hackney, "~> 1.18"}, - {:poolboy, "~> 1.5.2"} + {:poolboy, "~> 1.5.2"}, + {:logger_json, "~> 5.1"} ] end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs index 4f59274e8533..100ed3756e3e 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs @@ -32,29 +32,55 @@ defmodule EthereumJSONRPC.BlockTest do "uncles" => [] }) - assert result == %{ - difficulty: 17_561_410_778, - extra_data: "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32", - gas_limit: 5000, - gas_used: 0, - hash: "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623", - logs_bloom: - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - mix_hash: "0xbbb93d610b2b0296a59f18474ac3d6086a9902aa7ca4b9a306692f7c3d496fdf", - miner_hash: "0xbb7b8287f3f0a933474a79eae42cbca977791171", - nonce: 5_539_500_215_739_777_653, - number: 59, - parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e", - receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - size: 542, - state_root: "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba", - timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), - total_difficulty: nil, - transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: [], - withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" - } + assert result == + %{ + difficulty: 17_561_410_778, + extra_data: "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32", + gas_limit: 5000, + gas_used: 0, + hash: "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623", + logs_bloom: + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + mix_hash: "0xbbb93d610b2b0296a59f18474ac3d6086a9902aa7ca4b9a306692f7c3d496fdf", + miner_hash: "0xbb7b8287f3f0a933474a79eae42cbca977791171", + nonce: 5_539_500_215_739_777_653, + number: 59, + parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e", + receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + size: 542, + state_root: "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba", + timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), + total_difficulty: nil, + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + uncles: [] + } + |> (&if(Application.get_env(:explorer, :chain_type) == "rsk", + do: + Map.merge( + &1, + %{ + bitcoin_merged_mining_coinbase_transaction: nil, + bitcoin_merged_mining_header: nil, + bitcoin_merged_mining_merkle_proof: nil, + hash_for_merged_mining: nil, + minimum_gas_price: nil + } + ), + else: &1 + )).() + |> (&if(Application.get_env(:explorer, :chain_type) == "ethereum", + do: + Map.merge( + &1, + %{ + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + blob_gas_used: 0, + excess_blob_gas: 0 + } + ), + else: &1 + )).() end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs index ef2f58ed4a37..76100d469b3c 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs @@ -5,6 +5,8 @@ defmodule EthereumJSONRPC.ContractTest do import Mox + setup :verify_on_exit! + describe "execute_contract_functions/3" do test "executes the functions with and without the block_number, returns results in order" do json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs index 38f87e08af4c..7539b8e582ba 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/encoder_test.exs @@ -27,6 +27,38 @@ defmodule EthereumJSONRPC.EncoderTest do "0x9507d39a000000000000000000000000000000000000000000000000000000000000000a" end + test "generates the correct encoding with string argument" do + function_selector = %ABI.FunctionSelector{ + function: "isNewsletterCoverFullyClaimed", + input_names: ["newsletterId"], + inputs_indexed: nil, + return_names: [""], + returns: [:bool], + state_mutability: :view, + type: :function, + types: [:string] + } + + assert Encoder.encode_function_call(function_selector, ["6564f5623e2a9f0001cb7fee"]) == + "0xa07a712d000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000183635363466353632336532613966303030316362376665650000000000000000" + end + + test "generates the correct encoding with string started with 0x" do + function_selector = %ABI.FunctionSelector{ + function: "isNewsletterCoverFullyClaimed", + input_names: ["newsletterId"], + inputs_indexed: nil, + return_names: [""], + returns: [:bool], + state_mutability: :view, + type: :function, + types: [:string] + } + + assert Encoder.encode_function_call(function_selector, ["0x123"]) == + "0xa07a712d000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000053078313233000000000000000000000000000000000000000000000000000000" + end + test "generates the correct encoding with addresses arguments" do function_selector = %ABI.FunctionSelector{ function: "tokens", diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs new file mode 100644 index 000000000000..3e6d46dd8d5c --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs @@ -0,0 +1,200 @@ +defmodule EthereumJSONRPC.FilecoinTest do + use EthereumJSONRPC.Case, async: false + + import Mox + + alias EthereumJSONRPC.Filecoin + + setup :verify_on_exit! + + describe "fetch_block_internal_transactions/2" do + setup do + initial_env = Application.get_all_env(:ethereum_jsonrpc) + old_env = Application.get_env(:explorer, :chain_type) + + Application.put_env(:explorer, :chain_type, "filecoin") + + on_exit(fn -> + Application.put_all_env([{:ethereum_jsonrpc, initial_env}]) + Application.put_env(:explorer, :chain_type, old_env) + end) + + EthereumJSONRPC.Case.Filecoin.Mox.setup() + end + + setup :verify_on_exit! + + test "is supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_663_376 + block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^block_quantity]}], _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "type" => "call", + "subtraces" => 0, + "traceAddress" => [], + "action" => %{ + "callType" => "call", + "from" => "0xff0000000000000000000000000000000021cc23", + "to" => "0xff000000000000000000000000000000001a34e5", + "gas" => "0x1891a7d", + "value" => "0x0", + "input" => + "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850d8182004081820d58c0960ee115a7a4b6f2fd36a83da26c608d49e4160a3737655d0f637b81be81b018539809d35519b0b75ca06304b3b4d40c810e50b954e82c5119a8b4a64c3e762a7ae8a2d465d1cd5bf096c87c56ab0da879568378e5a2368c902eea9898cf1e2a1974ddb479ec6257b69aca7734d3b3e1e70428c77f9e528ffcb3dc3f050f0193c2cc005927a765c39a4931d67fb29aaba6e99f2c7d2566b98fdbf30d6e15a2bbd63b8fa059cfad231ccba1d8964542b50419eaad4bc442d3a1dc1f41941944c11a0037e5f45820d41114bb6abbf966c2528f5705447a53ee37b7055cd4478503ea5eaf1fe165c60000000000000000000000000000" + }, + "result" => %{ + "gasUsed" => "0x14696c1", + "output" => + "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "blockHash" => "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber" => 3_663_376, + "transactionHash" => "0xf37d8b8bf67df3ddaa264e22322d2b092e390ed33f1ab14c8a136b2767979254", + "transactionPosition" => 1 + }, + %{ + "type" => "call", + "subtraces" => 0, + "traceAddress" => [ + 1 + ], + "action" => %{ + "callType" => "call", + "from" => "0xff000000000000000000000000000000002c2c61", + "to" => "0xff00000000000000000000000000000000000004", + "gas" => "0x2c6aae6", + "value" => "0x0", + "input" => + "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000" + }, + "result" => %{ + "gasUsed" => "0x105fb2", + "output" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000" + }, + "blockHash" => "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber" => 3_663_376, + "transactionHash" => "0xbc62a61e0be0e8f6ae09e21ad10f6d79c9a8b8ebc46f8ce076dc0dbe1d6ed4a9", + "transactionPosition" => 21 + } + ] + } + ]} + end) + + assert {:ok, + [ + %{ + block_number: ^block_number, + transaction_index: 21, + transaction_hash: "0xbc62a61e0be0e8f6ae09e21ad10f6d79c9a8b8ebc46f8ce076dc0dbe1d6ed4a9", + index: 0, + trace_address: [1], + type: "call", + call_type: "call", + from_address_hash: "0xff000000000000000000000000000000002c2c61", + to_address_hash: "0xff00000000000000000000000000000000000004", + gas: 46_574_310, + gas_used: 1_073_074, + input: + "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + output: + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000", + value: 0 + }, + %{ + block_number: ^block_number, + transaction_index: 1, + transaction_hash: "0xf37d8b8bf67df3ddaa264e22322d2b092e390ed33f1ab14c8a136b2767979254", + index: 0, + trace_address: [], + type: "call", + call_type: "call", + from_address_hash: "0xff0000000000000000000000000000000021cc23", + to_address_hash: "0xff000000000000000000000000000000001a34e5", + gas: 25_762_429, + gas_used: 21_403_329, + input: + "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850d8182004081820d58c0960ee115a7a4b6f2fd36a83da26c608d49e4160a3737655d0f637b81be81b018539809d35519b0b75ca06304b3b4d40c810e50b954e82c5119a8b4a64c3e762a7ae8a2d465d1cd5bf096c87c56ab0da879568378e5a2368c902eea9898cf1e2a1974ddb479ec6257b69aca7734d3b3e1e70428c77f9e528ffcb3dc3f050f0193c2cc005927a765c39a4931d67fb29aaba6e99f2c7d2566b98fdbf30d6e15a2bbd63b8fa059cfad231ccba1d8964542b50419eaad4bc442d3a1dc1f41941944c11a0037e5f45820d41114bb6abbf966c2528f5705447a53ee37b7055cd4478503ea5eaf1fe165c60000000000000000000000000000", + output: + "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + value: 0 + } + ]} = + Filecoin.fetch_block_internal_transactions( + [ + block_number + ], + json_rpc_named_arguments + ) + end + + test "parses smart-contract creation", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_663_377 + block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^block_quantity]}], _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "type" => "create", + "subtraces" => 0, + "traceAddress" => [ + 0 + ], + "action" => %{ + "from" => "0xff00000000000000000000000000000000000004", + "gas" => "0x53cf101", + "value" => "0x0", + "init" => "0xfe" + }, + "result" => %{ + "address" => "0xff000000000000000000000000000000002d44e6", + "gasUsed" => "0x1be32fc", + "code" => "0xfe" + }, + "blockHash" => "0xbeef70ac3db42f10dd1eb03f5f0640557acd72db61357cf3c4f47945d8beab79", + "blockNumber" => 3_663_377, + "transactionHash" => "0x86ccda9dc76bd37c7201a6da1e10260bf984590efc6b221635c8dd33cc520067", + "transactionPosition" => 18 + } + ] + } + ]} + end) + + assert {:ok, + [ + %{ + block_number: ^block_number, + transaction_index: 18, + transaction_hash: "0x86ccda9dc76bd37c7201a6da1e10260bf984590efc6b221635c8dd33cc520067", + index: 0, + trace_address: [0], + type: "create", + from_address_hash: "0xff00000000000000000000000000000000000004", + created_contract_address_hash: "0xff000000000000000000000000000000002d44e6", + gas: 87_879_937, + gas_used: 29_242_108, + init: "0xfe", + created_contract_code: "0xfe", + value: 0 + } + ]} = + Filecoin.fetch_block_internal_transactions( + [ + block_number + ], + json_rpc_named_arguments + ) + end + end +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs index 9f3620039ae2..ed1202cf3b12 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs @@ -5,14 +5,14 @@ defmodule EthereumJSONRPC.GethTest do alias EthereumJSONRPC.Geth - @moduletag :no_nethermind + setup :verify_on_exit! describe "fetch_internal_transactions/2" do # Infura Mainnet does not support debug_traceTransaction, so this cannot be tested expect in Mox setup do - EthereumJSONRPC.Case.Geth.Mox.setup() initial_env = Application.get_all_env(:ethereum_jsonrpc) on_exit(fn -> Application.put_all_env([{:ethereum_jsonrpc, initial_env}]) end) + EthereumJSONRPC.Case.Geth.Mox.setup() end setup :verify_on_exit! @@ -24,7 +24,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" tracer = File.read!("priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js") - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^transaction_hash, %{tracer: ^tracer}]}], _ -> + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^transaction_hash, %{"tracer" => ^tracer}]}], _ -> {:ok, [ %{ @@ -47,7 +47,7 @@ defmodule EthereumJSONRPC.GethTest do ]} end) - Application.put_env(:ethereum_jsonrpc, Geth, tracer: "js") + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "js", debug_trace_transaction_timeout: "5s") assert {:ok, [ @@ -96,7 +96,7 @@ defmodule EthereumJSONRPC.GethTest do tracer = File.read!("priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js") expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn - [%{id: id, params: [^transaction_hash, %{tracer: "callTracer"}]}], _ -> + [%{id: id, params: [^transaction_hash, %{"tracer" => "callTracer"}]}], _ -> {:ok, [ %{ @@ -220,7 +220,7 @@ defmodule EthereumJSONRPC.GethTest do end) expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn - [%{id: id, params: [^transaction_hash, %{tracer: ^tracer}]}], _ -> + [%{id: id, params: [^transaction_hash, %{"tracer" => ^tracer}]}], _ -> {:ok, [ %{ @@ -357,9 +357,11 @@ defmodule EthereumJSONRPC.GethTest do ]} end) + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + call_tracer_internal_txs = Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) - Application.put_env(:ethereum_jsonrpc, Geth, tracer: "js") + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "js", debug_trace_transaction_timeout: "5s") assert call_tracer_internal_txs == Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) @@ -381,7 +383,7 @@ defmodule EthereumJSONRPC.GethTest do tracer = File.read!("priv/js/ethereum_jsonrpc/geth/debug_traceTransaction/tracer.js") expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn - [%{id: id, params: [^transaction_hash, %{tracer: "callTracer"}]}], _ -> + [%{id: id, params: [^transaction_hash, %{"tracer" => "callTracer"}]}], _ -> {:ok, [ %{ @@ -403,7 +405,7 @@ defmodule EthereumJSONRPC.GethTest do end) expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn - [%{id: id, params: [^transaction_hash, %{tracer: ^tracer}]}], _ -> + [%{id: id, params: [^transaction_hash, %{"tracer" => ^tracer}]}], _ -> {:ok, [ %{ @@ -427,9 +429,11 @@ defmodule EthereumJSONRPC.GethTest do ]} end) + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + call_tracer_internal_txs = Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) - Application.put_env(:ethereum_jsonrpc, Geth, tracer: "js") + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "js", debug_trace_transaction_timeout: "5s") assert call_tracer_internal_txs == Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) @@ -465,6 +469,8 @@ defmodule EthereumJSONRPC.GethTest do ]} end) + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + assert {:ok, [ %{ @@ -480,11 +486,395 @@ defmodule EthereumJSONRPC.GethTest do } ]} = Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) end + + test "uppercase type parsing result is the same as lowercase", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + transaction_hash = "0xb342cafc6ac552c3be2090561453204c8784caf025ac8267320834e4cd163d96" + block_number = 3_287_375 + transaction_index = 13 + + transaction_params = %{ + block_number: block_number, + transaction_index: transaction_index, + hash_data: transaction_hash + } + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn + [%{id: id, params: [^transaction_hash, %{"tracer" => "callTracer"}]}], _ -> + {:ok, + [ + %{ + id: id, + result: %{ + "type" => "CREATE", + "from" => "0x117b358218da5a4f647072ddb50ded038ed63d17", + "to" => "0x205a6b72ce16736c9d87172568a9c0cb9304de0d", + "value" => "0x0", + "gas" => "0x106f5", + "gasUsed" => "0x106f5", + "input" => + "0x608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033", + "output" => + "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033" + } + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + + uppercase_result = Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn + [%{id: id, params: [^transaction_hash, %{"tracer" => "callTracer"}]}], _ -> + {:ok, + [ + %{ + id: id, + result: %{ + "type" => "create", + "from" => "0x117b358218da5a4f647072ddb50ded038ed63d17", + "to" => "0x205a6b72ce16736c9d87172568a9c0cb9304de0d", + "value" => "0x0", + "gas" => "0x106f5", + "gasUsed" => "0x106f5", + "input" => + "0x608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033", + "output" => + "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033" + } + } + ]} + end) + + lowercase_result = Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) + + assert uppercase_result == lowercase_result + end end describe "fetch_block_internal_transactions/1" do - test "is not supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do - EthereumJSONRPC.Geth.fetch_block_internal_transactions([], json_rpc_named_arguments) + setup do + EthereumJSONRPC.Case.Geth.Mox.setup() + end + + test "is supported", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_287_375 + block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) + transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^block_quantity, %{"tracer" => "callTracer"}]}], + _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => transaction_hash + } + ] + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + + assert {:ok, + [ + %{ + block_number: 3_287_375, + call_type: "call", + from_address_hash: "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + gas: 1_000_000, + gas_used: 46841, + index: 0, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x4200000000000000000000000000000000000015", + trace_address: [], + transaction_hash: ^transaction_hash, + transaction_index: 0, + type: "call", + value: 0 + }, + %{ + block_number: 3_287_375, + call_type: "delegatecall", + from_address_hash: "0x4200000000000000000000000000000000000015", + gas: 956_988, + gas_used: 18984, + index: 1, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + trace_address: [0], + transaction_hash: ^transaction_hash, + transaction_index: 0, + type: "call", + value: 0 + } + ]} = Geth.fetch_block_internal_transactions([block_number], json_rpc_named_arguments) + end + + test "works for multiple blocks request", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number_1 = 3_287_375 + block_number_2 = 3_287_376 + block_quantity_1 = EthereumJSONRPC.integer_to_quantity(block_number_1) + block_quantity_2 = EthereumJSONRPC.integer_to_quantity(block_number_2) + transaction_hash_1 = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + transaction_hash_2 = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5b" + + expect(EthereumJSONRPC.Mox, :json_rpc, fn + [ + %{id: id_1, params: [^block_quantity_1, %{"tracer" => "callTracer"}]}, + %{id: id_2, params: [^block_quantity_2, %{"tracer" => "callTracer"}]} + ], + _ -> + {:ok, + [ + %{ + id: id_1, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => transaction_hash_1 + } + ] + }, + %{ + id: id_2, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => transaction_hash_2 + } + ] + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + + assert {:ok, + [ + %{ + block_number: ^block_number_1, + call_type: "call", + from_address_hash: "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + gas: 1_000_000, + gas_used: 46841, + index: 0, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x4200000000000000000000000000000000000015", + trace_address: [], + transaction_hash: ^transaction_hash_1, + transaction_index: 0, + type: "call", + value: 0 + }, + %{ + block_number: ^block_number_1, + call_type: "delegatecall", + from_address_hash: "0x4200000000000000000000000000000000000015", + gas: 956_988, + gas_used: 18984, + index: 1, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + trace_address: [0], + transaction_hash: ^transaction_hash_1, + transaction_index: 0, + type: "call", + value: 0 + }, + %{ + block_number: ^block_number_2, + call_type: "call", + from_address_hash: "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + gas: 1_000_000, + gas_used: 46841, + index: 0, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x4200000000000000000000000000000000000015", + trace_address: [], + transaction_hash: ^transaction_hash_2, + transaction_index: 0, + type: "call", + value: 0 + }, + %{ + block_number: ^block_number_2, + call_type: "delegatecall", + from_address_hash: "0x4200000000000000000000000000000000000015", + gas: 956_988, + gas_used: 18984, + index: 1, + input: + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + output: "0x", + to_address_hash: "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + trace_address: [0], + transaction_hash: ^transaction_hash_2, + transaction_index: 0, + type: "call", + value: 0 + } + ]} = Geth.fetch_block_internal_transactions([block_number_1, block_number_2], json_rpc_named_arguments) + end + + test "result is the same as fetch_internal_transactions/2", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_number = 3_287_375 + block_quantity = EthereumJSONRPC.integer_to_quantity(block_number) + transaction_hash = "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + + expect(EthereumJSONRPC.Mox, :json_rpc, 2, fn + [%{id: id, params: [^block_quantity, %{"tracer" => "callTracer"}]}], _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => transaction_hash + } + ] + } + ]} + + [%{id: id, params: [^transaction_hash, %{"tracer" => "callTracer"}]}], _ -> + {:ok, + [ + %{ + id: id, + result: %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + } + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, Geth, tracer: "call_tracer", debug_trace_transaction_timeout: "5s") + + assert Geth.fetch_block_internal_transactions([block_number], json_rpc_named_arguments) == + Geth.fetch_internal_transactions( + [%{block_number: block_number, transaction_index: 0, hash_data: transaction_hash}], + json_rpc_named_arguments + ) end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs index f554c31ce572..945f3f78bc94 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs @@ -23,7 +23,6 @@ defmodule EthereumJSONRPC.ReceiptsTest do index: index, first_topic: first_topic, status: status, - type: type, transaction_hash: transaction_hash, transaction_index: transaction_index } = @@ -41,7 +40,6 @@ defmodule EthereumJSONRPC.ReceiptsTest do first_topic: "0xf6db2bace4ac8277384553ad9603d045220a91fb2448ab6130d7a6f044f9a8cf", gas_used: 106_025, status: nil, - type: nil, transaction_hash: "0xd3efddbbeb6ad8d8bb3f6b8c8fb6165567e9dd868013146bdbeb60953c82822a", transaction_index: 17 } @@ -58,7 +56,6 @@ defmodule EthereumJSONRPC.ReceiptsTest do index: 0, first_topic: "0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22", status: :ok, - type: "mined", transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", transaction_index: 0 } @@ -89,8 +86,7 @@ defmodule EthereumJSONRPC.ReceiptsTest do "data" => data, "logIndex" => integer_to_quantity(index), "topics" => [first_topic], - "transactionHash" => transaction_hash, - "type" => type + "transactionHash" => transaction_hash } ], "status" => native_status, diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs index efda05ab86e8..0eaa6c1f3159 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs @@ -26,28 +26,35 @@ defmodule EthereumJSONRPC.RequestCoordinatorTest do describe "perform/4" do test "forwards result whenever a request doesn't timeout", %{timeout_table: timeout_table} do expect(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> {:ok, %{}} end) - assert RollingWindow.count(timeout_table, :throttleable_error_count) == 0 - assert {:ok, %{}} == RequestCoordinator.perform(%{}, EthereumJSONRPC.Mox, [], :timer.minutes(60)) - assert RollingWindow.count(timeout_table, :throttleable_error_count) == 0 + assert RollingWindow.count(timeout_table, :throttleable_error_count_eth_call) == 0 + + assert {:ok, %{}} == + RequestCoordinator.perform(%{method: "eth_call"}, EthereumJSONRPC.Mox, [], :timer.minutes(60)) + + assert RollingWindow.count(timeout_table, :throttleable_error_count_eth_call) == 0 end test "increments counter on certain errors", %{timeout_table: timeout_table} do - expect(EthereumJSONRPC.Mox, :json_rpc, fn :timeout, _ -> {:error, :timeout} end) - expect(EthereumJSONRPC.Mox, :json_rpc, fn :bad_gateway, _ -> {:error, {:bad_gateway, "message"}} end) + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{method: "timeout"}, _ -> {:error, :timeout} end) + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{method: "bad_gateway"}, _ -> {:error, {:bad_gateway, "message"}} end) + + assert {:error, :timeout} == + RequestCoordinator.perform(%{method: "timeout"}, EthereumJSONRPC.Mox, [], :timer.minutes(60)) - assert {:error, :timeout} == RequestCoordinator.perform(:timeout, EthereumJSONRPC.Mox, [], :timer.minutes(60)) - assert RollingWindow.count(timeout_table, :throttleable_error_count) == 1 + assert RollingWindow.count(timeout_table, :throttleable_error_count_timeout) == 1 + assert RollingWindow.count(timeout_table, :throttleable_error_count_bad_gateway) == 0 assert {:error, {:bad_gateway, "message"}} == - RequestCoordinator.perform(:bad_gateway, EthereumJSONRPC.Mox, [], :timer.minutes(60)) + RequestCoordinator.perform(%{method: "bad_gateway"}, EthereumJSONRPC.Mox, [], :timer.minutes(60)) - assert RollingWindow.count(timeout_table, :throttleable_error_count) == 2 + assert RollingWindow.count(timeout_table, :throttleable_error_count_timeout) == 1 + assert RollingWindow.count(timeout_table, :throttleable_error_count_bad_gateway) == 1 end test "returns timeout error if sleep time will exceed max timeout", %{timeout_table: timeout_table} do expect(EthereumJSONRPC.Mox, :json_rpc, 0, fn _, _ -> :ok end) - RollingWindow.inc(timeout_table, :throttleable_error_count) - assert {:error, :timeout} == RequestCoordinator.perform(%{}, EthereumJSONRPC.Mox, [], 1) + RollingWindow.inc(timeout_table, :throttleable_error_count_eth_call) + assert {:error, :timeout} == RequestCoordinator.perform(%{method: "eth_call"}, EthereumJSONRPC.Mox, [], 1) end test "increments throttle_table even when not an error", %{throttle_table: throttle_table} do diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/filecoin/mox.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/filecoin/mox.ex new file mode 100644 index 000000000000..12510755ad9e --- /dev/null +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/filecoin/mox.ex @@ -0,0 +1,17 @@ +defmodule EthereumJSONRPC.Case.Filecoin.Mox do + @moduledoc """ + `EthereumJSONRPC.Case` for mocking connecting to Filecoin using `Mox` + """ + + def setup do + %{ + block_interval: 500, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.Mox, + transport_options: [http_options: [timeout: 60000, recv_timeout: 60000]], + variant: EthereumJSONRPC.Filecoin + ], + subscribe_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: []] + } + end +end diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/mox.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/mox.ex index d1f113223d68..41a2593cb946 100644 --- a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/mox.ex +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/mox.ex @@ -6,7 +6,11 @@ defmodule EthereumJSONRPC.Case.Geth.Mox do def setup do %{ block_interval: 500, - json_rpc_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: [], variant: EthereumJSONRPC.Geth], + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.Mox, + transport_options: [http_options: [timeout: 60000, recv_timeout: 60000]], + variant: EthereumJSONRPC.Geth + ], subscribe_named_arguments: [transport: EthereumJSONRPC.Mox, transport_options: []] } end diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index feb7ca93e8b3..2c1e69d061b6 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -11,6 +11,7 @@ import Config # General application configuration config :explorer, + chain_type: ConfigHelper.chain_type(), ecto_repos: ConfigHelper.repos(), token_functions_reader_max_retries: 3, # for not fully indexed blockchains @@ -75,6 +76,11 @@ config :explorer, Explorer.Chain.Cache.WithdrawalsSum, enable_consolidation: true, update_interval_in_milliseconds: update_interval_in_milliseconds_default +config :explorer, Explorer.Chain.Cache.StabilityValidatorsCounters, + enabled: true, + enable_consolidation: true, + update_interval_in_milliseconds: update_interval_in_milliseconds_default + config :explorer, Explorer.Chain.Cache.TransactionActionTokensData, enabled: true config :explorer, Explorer.Chain.Cache.TransactionActionUniswapPools, enabled: true @@ -100,7 +106,7 @@ config :explorer, Explorer.Counters.AddressTokenTransfersCounter, enabled: true, enable_consolidation: true -config :explorer, Explorer.Counters.BlockBurnedFeeCounter, +config :explorer, Explorer.Counters.BlockBurntFeeCounter, enabled: true, enable_consolidation: true @@ -108,10 +114,15 @@ config :explorer, Explorer.Counters.BlockPriorityFeeCounter, enabled: true, enable_consolidation: true -config :explorer, Explorer.TokenTransferTokenIdMigration.Supervisor, enabled: true - config :explorer, Explorer.TokenInstanceOwnerAddressMigration.Supervisor, enabled: true +config :explorer, Explorer.Migrator.TransactionsDenormalization, enabled: true +config :explorer, Explorer.Migrator.AddressCurrentTokenBalanceTokenType, enabled: true +config :explorer, Explorer.Migrator.AddressTokenBalanceTokenType, enabled: true +config :explorer, Explorer.Migrator.SanitizeMissingBlockRanges, enabled: true +config :explorer, Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers, enabled: true +config :explorer, Explorer.Migrator.TokenTransferTokenType, enabled: true + config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: true config :explorer, Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand, enabled: true @@ -135,6 +146,8 @@ config :explorer, config :explorer, :http_adapter, HTTPoison +config :explorer, Explorer.Chain.BridgedToken, enabled: ConfigHelper.parse_bool_env_var("BRIDGED_TOKENS_ENABLED") + config :logger, :explorer, # keep synced with `config/config.exs` format: "$dateT$time $metadata[$level] $message\n", diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index 0aa303a2bfab..a387ee24220c 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -11,16 +11,32 @@ config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(80) # Configure Account database config :explorer, Explorer.Repo.Account, timeout: :timer.seconds(80) +# Configure Optimism database +config :explorer, Explorer.Repo.Optimism, timeout: :timer.seconds(80) + # Configure Polygon Edge database config :explorer, Explorer.Repo.PolygonEdge, timeout: :timer.seconds(80) # Configure Polygon zkEVM database config :explorer, Explorer.Repo.PolygonZkevm, timeout: :timer.seconds(80) +# Configure ZkSync database +config :explorer, Explorer.Repo.ZkSync, timeout: :timer.seconds(80) + config :explorer, Explorer.Repo.RSK, timeout: :timer.seconds(80) +config :explorer, Explorer.Repo.Shibarium, timeout: :timer.seconds(80) + config :explorer, Explorer.Repo.Suave, timeout: :timer.seconds(80) +config :explorer, Explorer.Repo.Beacon, timeout: :timer.seconds(80) + +config :explorer, Explorer.Repo.BridgedTokens, timeout: :timer.seconds(80) + +config :explorer, Explorer.Repo.Filecoin, timeout: :timer.seconds(80) + +config :explorer, Explorer.Repo.Stability, timeout: :timer.seconds(80) + config :explorer, Explorer.Tracer, env: "dev", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/dev/arbitrum.exs b/apps/explorer/config/dev/arbitrum.exs index 1f7cd963e91e..758414fc4d52 100644 --- a/apps/explorer/config/dev/arbitrum.exs +++ b/apps/explorer/config/dev/arbitrum.exs @@ -14,6 +14,10 @@ config :explorer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545") + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Arbitrum diff --git a/apps/explorer/config/dev/besu.exs b/apps/explorer/config/dev/besu.exs index 64db4d5b8180..9b32da34d7b0 100644 --- a/apps/explorer/config/dev/besu.exs +++ b/apps/explorer/config/dev/besu.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" ], diff --git a/apps/explorer/config/dev/erigon.exs b/apps/explorer/config/dev/erigon.exs index 9253f8ea3081..5304055919e5 100644 --- a/apps/explorer/config/dev/erigon.exs +++ b/apps/explorer/config/dev/erigon.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" ], diff --git a/apps/explorer/config/dev/filecoin.exs b/apps/explorer/config/dev/filecoin.exs new file mode 100644 index 000000000000..ea75c71dc462 --- /dev/null +++ b/apps/explorer/config/dev/filecoin.exs @@ -0,0 +1,34 @@ +import Config + +~w(config config_helper.exs) +|> Path.join() +|> Code.eval_file() + +hackney_opts = ConfigHelper.hackney_options() +timeout = ConfigHelper.timeout(1) + +config :explorer, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:1234/rpc/v1", + fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:1234/rpc/v1"), + trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:1234/rpc/v1" + ], + http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + ], + variant: EthereumJSONRPC.Filecoin + ], + subscribe_named_arguments: [ + transport: EthereumJSONRPC.WebSocket, + transport_options: [ + web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, + url: System.get_env("ETHEREUM_JSONRPC_WS_URL") + ], + variant: EthereumJSONRPC.Filecoin + ] diff --git a/apps/explorer/config/dev/ganache.exs b/apps/explorer/config/dev/ganache.exs index 8f399f532ff5..ae03df000e59 100644 --- a/apps/explorer/config/dev/ganache.exs +++ b/apps/explorer/config/dev/ganache.exs @@ -14,6 +14,10 @@ config :explorer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:7545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:7545") + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Ganache diff --git a/apps/explorer/config/dev/geth.exs b/apps/explorer/config/dev/geth.exs index 4ac8203aecac..9d9a166f6329 100644 --- a/apps/explorer/config/dev/geth.exs +++ b/apps/explorer/config/dev/geth.exs @@ -15,8 +15,11 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), + debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", + debug_traceBlockByNumber: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], diff --git a/apps/explorer/config/dev/nethermind.exs b/apps/explorer/config/dev/nethermind.exs index e92b980507f0..95ce54a92406 100644 --- a/apps/explorer/config/dev/nethermind.exs +++ b/apps/explorer/config/dev/nethermind.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" ], diff --git a/apps/explorer/config/dev/rsk.exs b/apps/explorer/config/dev/rsk.exs index 597d78c395a0..74cb6d98f095 100644 --- a/apps/explorer/config/dev/rsk.exs +++ b/apps/explorer/config/dev/rsk.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" ], diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index e14afe322c04..27fa8cad9575 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -16,6 +16,10 @@ config :explorer, Explorer.Repo.Account, prepare: :unnamed, timeout: :timer.seconds(60) +config :explorer, Explorer.Repo.Optimism, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Repo.PolygonEdge, prepare: :unnamed, timeout: :timer.seconds(60) @@ -24,14 +28,38 @@ config :explorer, Explorer.Repo.PolygonZkevm, prepare: :unnamed, timeout: :timer.seconds(60) +config :explorer, Explorer.Repo.ZkSync, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Repo.RSK, prepare: :unnamed, timeout: :timer.seconds(60) +config :explorer, Explorer.Repo.Shibarium, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Repo.Suave, prepare: :unnamed, timeout: :timer.seconds(60) +config :explorer, Explorer.Repo.Beacon, + prepare: :unnamed, + timeout: :timer.seconds(60) + +config :explorer, Explorer.Repo.BridgedTokens, + prepare: :unnamed, + timeout: :timer.seconds(60) + +config :explorer, Explorer.Repo.Filecoin, + prepare: :unnamed, + timeout: :timer.seconds(60) + +config :explorer, Explorer.Repo.Stability, + prepare: :unnamed, + timeout: :timer.seconds(60) + config :explorer, Explorer.Tracer, env: "production", disabled?: true config :logger, :explorer, diff --git a/apps/explorer/config/prod/arbitrum.exs b/apps/explorer/config/prod/arbitrum.exs index ea4af81646f2..2ce0af488a63 100644 --- a/apps/explorer/config/prod/arbitrum.exs +++ b/apps/explorer/config/prod/arbitrum.exs @@ -14,6 +14,10 @@ config :explorer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url() + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Arbitrum diff --git a/apps/explorer/config/prod/besu.exs b/apps/explorer/config/prod/besu.exs index 26df9c5bd480..cb641e7244ec 100644 --- a/apps/explorer/config/prod/besu.exs +++ b/apps/explorer/config/prod/besu.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ], diff --git a/apps/explorer/config/prod/erigon.exs b/apps/explorer/config/prod/erigon.exs index 0a954a88b1df..e574b5564663 100644 --- a/apps/explorer/config/prod/erigon.exs +++ b/apps/explorer/config/prod/erigon.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ], diff --git a/apps/explorer/config/prod/filecoin.exs b/apps/explorer/config/prod/filecoin.exs new file mode 100644 index 000000000000..00c056dbe6dd --- /dev/null +++ b/apps/explorer/config/prod/filecoin.exs @@ -0,0 +1,34 @@ +import Config + +~w(config config_helper.exs) +|> Path.join() +|> Code.eval_file() + +hackney_opts = ConfigHelper.hackney_options() +timeout = ConfigHelper.timeout(1) + +config :explorer, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), + fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url(), + trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") + ], + http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + ], + variant: EthereumJSONRPC.Filecoin + ], + subscribe_named_arguments: [ + transport: EthereumJSONRPC.WebSocket, + transport_options: [ + web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, + url: System.get_env("ETHEREUM_JSONRPC_WS_URL") + ], + variant: EthereumJSONRPC.Filecoin + ] diff --git a/apps/explorer/config/prod/ganache.exs b/apps/explorer/config/prod/ganache.exs index 25de1e5b9a31..7a0581452b3a 100644 --- a/apps/explorer/config/prod/ganache.exs +++ b/apps/explorer/config/prod/ganache.exs @@ -14,6 +14,10 @@ config :explorer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url() + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Ganache diff --git a/apps/explorer/config/prod/geth.exs b/apps/explorer/config/prod/geth.exs index 3a81acee9c00..90a1a7bb3e80 100644 --- a/apps/explorer/config/prod/geth.exs +++ b/apps/explorer/config/prod/geth.exs @@ -15,8 +15,11 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") + eth_call: ConfigHelper.eth_call_url(), + debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), + debug_traceBlockByNumber: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], diff --git a/apps/explorer/config/prod/nethermind.exs b/apps/explorer/config/prod/nethermind.exs index 4057a0d99d03..a599d352ab41 100644 --- a/apps/explorer/config/prod/nethermind.exs +++ b/apps/explorer/config/prod/nethermind.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ], diff --git a/apps/explorer/config/prod/rsk.exs b/apps/explorer/config/prod/rsk.exs index 9f65d8be864f..ce19c3138079 100644 --- a/apps/explorer/config/prod/rsk.exs +++ b/apps/explorer/config/prod/rsk.exs @@ -15,8 +15,9 @@ config :explorer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ], diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 9449463a5b9b..d9cfc1a821b6 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -18,6 +18,9 @@ config :explorer, Explorer.Chain.Transaction.History.Historian, enabled: false config :explorer, Explorer.Market.History.Historian, enabled: false config :explorer, Explorer.Counters.AddressesCounter, enabled: false, enable_consolidation: false +config :explorer, Explorer.Counters.LastOutputRootSizeCounter, enabled: false, enable_consolidation: false +config :explorer, Explorer.Counters.Transactions24hStats, enabled: false, enable_consolidation: false +config :explorer, Explorer.Counters.FreshPendingTransactionsCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Chain.Cache.ContractsCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Chain.Cache.NewContractsCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Chain.Cache.VerifiedContractsCounter, enabled: false, enable_consolidation: false @@ -33,10 +36,15 @@ config :explorer, Explorer.Market.History.Cataloger, enabled: false config :explorer, Explorer.Tracer, disabled?: false -config :explorer, Explorer.TokenTransferTokenIdMigration.Supervisor, enabled: false - config :explorer, Explorer.TokenInstanceOwnerAddressMigration.Supervisor, enabled: false +config :explorer, Explorer.Migrator.TransactionsDenormalization, enabled: false +config :explorer, Explorer.Migrator.AddressCurrentTokenBalanceTokenType, enabled: false +config :explorer, Explorer.Migrator.AddressTokenBalanceTokenType, enabled: false +config :explorer, Explorer.Migrator.SanitizeMissingBlockRanges, enabled: false +config :explorer, Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers, enabled: false +config :explorer, Explorer.Migrator.TokenTransferTokenType, enabled: false + config :explorer, realtime_events_sender: Explorer.Chain.Events.SimpleSender diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 8a08ee60acf5..9ace0f12c261 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -12,7 +12,8 @@ config :explorer, Explorer.Repo, ownership_timeout: :timer.minutes(7), timeout: :timer.seconds(60), queue_target: 1000, - migration_lock: nil + migration_lock: nil, + log: false # Configure API database config :explorer, Explorer.Repo.Replica1, @@ -23,9 +24,12 @@ config :explorer, Explorer.Repo.Replica1, ownership_timeout: :timer.minutes(1), timeout: :timer.seconds(60), queue_target: 1000, - enable_caching_implementation_data_of_proxy: true, - avg_block_time_as_ttl_cached_implementation_data_of_proxy: false, - fallback_ttl_cached_implementation_data_of_proxy: :timer.seconds(20), + log: false + +config :explorer, :proxy, + caching_implementation_data_enabled: true, + implementation_data_ttl_via_avg_block_time: false, + fallback_cached_implementation_data_ttl: :timer.seconds(20), implementation_data_fetching_timeout: :timer.seconds(20) # Configure API database @@ -36,9 +40,22 @@ config :explorer, Explorer.Repo.Account, # Default of `5_000` was too low for `BlockFetcher` test ownership_timeout: :timer.minutes(1), timeout: :timer.seconds(60), - queue_target: 1000 + queue_target: 1000, + log: false -for repo <- [Explorer.Repo.PolygonEdge, Explorer.Repo.PolygonZkevm, Explorer.Repo.RSK, Explorer.Repo.Suave] do +for repo <- [ + Explorer.Repo.Beacon, + Explorer.Repo.Optimism, + Explorer.Repo.PolygonEdge, + Explorer.Repo.PolygonZkevm, + Explorer.Repo.ZkSync, + Explorer.Repo.RSK, + Explorer.Repo.Shibarium, + Explorer.Repo.Suave, + Explorer.Repo.BridgedTokens, + Explorer.Repo.Filecoin, + Explorer.Repo.Stability + ] do config :explorer, repo, database: "explorer_test", hostname: "localhost", @@ -46,9 +63,20 @@ for repo <- [Explorer.Repo.PolygonEdge, Explorer.Repo.PolygonZkevm, Explorer.Rep # Default of `5_000` was too low for `BlockFetcher` test ownership_timeout: :timer.minutes(1), timeout: :timer.seconds(60), - queue_target: 1000 + queue_target: 1000, + log: false, + pool_size: 1 end +config :explorer, Explorer.Repo.PolygonZkevm, + database: "explorer_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox, + # Default of `5_000` was too low for `BlockFetcher` test + ownership_timeout: :timer.minutes(1), + timeout: :timer.seconds(60), + queue_target: 1000 + config :logger, :explorer, level: :warn, path: Path.absname("logs/test/explorer.log") diff --git a/apps/explorer/config/test/filecoin.exs b/apps/explorer/config/test/filecoin.exs new file mode 100644 index 000000000000..e25ea90700c9 --- /dev/null +++ b/apps/explorer/config/test/filecoin.exs @@ -0,0 +1,13 @@ +import Config + +config :explorer, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.Mox, + transport_options: [], + variant: EthereumJSONRPC.Filecoin + ], + subscribe_named_arguments: [ + transport: EthereumJSONRPC.Mox, + transport_options: [], + variant: EthereumJSONRPC.Filecoin + ] diff --git a/apps/explorer/lib/explorer/account/api/key.ex b/apps/explorer/lib/explorer/account/api/key.ex index ab80d41ec234..5330ab98a461 100644 --- a/apps/explorer/lib/explorer/account/api/key.ex +++ b/apps/explorer/lib/explorer/account/api/key.ex @@ -13,10 +13,10 @@ defmodule Explorer.Account.Api.Key do @max_key_per_account 3 @primary_key false - schema "account_api_keys" do - field(:name, :string) - field(:value, UUID, primary_key: true) - belongs_to(:identity, Identity) + typed_schema "account_api_keys" do + field(:name, :string, null: false) + field(:value, UUID, primary_key: true, null: false) + belongs_to(:identity, Identity, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/account/api/plan.ex b/apps/explorer/lib/explorer/account/api/plan.ex index b89b33bc9cb7..599f133caa2f 100644 --- a/apps/explorer/lib/explorer/account/api/plan.ex +++ b/apps/explorer/lib/explorer/account/api/plan.ex @@ -4,7 +4,7 @@ defmodule Explorer.Account.Api.Plan do """ use Explorer.Schema - schema "account_api_plans" do + typed_schema "account_api_plans" do field(:name, :string) field(:max_req_per_second, :integer) diff --git a/apps/explorer/lib/explorer/account/custom_abi.ex b/apps/explorer/lib/explorer/account/custom_abi.ex index b48596b7ceec..5784c8586a45 100644 --- a/apps/explorer/lib/explorer/account/custom_abi.ex +++ b/apps/explorer/lib/explorer/account/custom_abi.ex @@ -14,15 +14,15 @@ defmodule Explorer.Account.CustomABI do @max_abis_per_account 15 - schema "account_custom_abis" do - field(:abi, {:array, :map}) + typed_schema "account_custom_abis" do + field(:abi, {:array, :map}, null: false) field(:given_abi, :string, virtual: true) field(:abi_validating_error, :string, virtual: true) - field(:address_hash_hash, Cloak.Ecto.SHA256) - field(:address_hash, Explorer.Encrypted.AddressHash) - field(:name, Explorer.Encrypted.Binary) + field(:address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) + field(:name, Explorer.Encrypted.Binary, null: false) - belongs_to(:identity, Identity) + belongs_to(:identity, Identity, null: false) timestamps() end @@ -65,7 +65,7 @@ defmodule Explorer.Account.CustomABI do defp check_smart_contract_address(custom_abi), do: custom_abi defp check_smart_contract_address_inner(changeset, address_hash) do - if Chain.is_address_hash_is_smart_contract?(address_hash) do + if Chain.address_hash_is_smart_contract?(address_hash) do changeset else add_error(changeset, :address_hash, "Address is not a smart contract") diff --git a/apps/explorer/lib/explorer/account/identity.ex b/apps/explorer/lib/explorer/account/identity.ex index 05171bc9f79d..1fd3300db5c1 100644 --- a/apps/explorer/lib/explorer/account/identity.ex +++ b/apps/explorer/lib/explorer/account/identity.ex @@ -10,11 +10,11 @@ defmodule Explorer.Account.Identity do alias Explorer.Account.Api.Plan alias Explorer.Account.{TagAddress, Watchlist} - schema "account_identities" do - field(:uid_hash, Cloak.Ecto.SHA256) - field(:uid, Explorer.Encrypted.Binary) - field(:email, Explorer.Encrypted.Binary) - field(:name, Explorer.Encrypted.Binary) + typed_schema "account_identities" do + field(:uid_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:uid, Explorer.Encrypted.Binary, null: false) + field(:email, Explorer.Encrypted.Binary, null: false) + field(:name, Explorer.Encrypted.Binary, null: false) field(:nickname, Explorer.Encrypted.Binary) field(:avatar, Explorer.Encrypted.Binary) field(:verification_email_sent_at, :utc_datetime_usec) diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex index 713e5c3f1beb..6e8639e058e2 100644 --- a/apps/explorer/lib/explorer/account/notifier/email.ex +++ b/apps/explorer/lib/explorer/account/notifier/email.ex @@ -59,6 +59,9 @@ defmodule Explorer.Account.Notifier.Email do "ERC-1155" -> "Token ID: " <> subject <> " of " + + "ERC-404" -> + "Token ID: " <> subject <> " of " end end diff --git a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex index 6022c87fcc7d..f977b3e4dcb2 100644 --- a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex +++ b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex @@ -5,16 +5,16 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] + alias Explorer.Chain.Address + @blacklist [ burn_address_hash_string(), "0x000000000000000000000000000000000000dEaD" ] - alias Explorer.{AccessHelper, Repo} - alias Explorer.Chain.Token + alias Explorer.AccessHelper - import Ecto.Query, only: [from: 2] - import Explorer.Chain, only: [string_to_address_hash: 1] + import Explorer.Chain, only: [string_to_address_hash: 1, hash_to_address: 1] def check(address_string) when is_bitstring(address_string) do case format_address(address_string) do @@ -31,8 +31,8 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do address_hash in blacklist() -> {:error, "This address is blacklisted"} - is_contract(address_hash) -> - {:error, "This address isn't personal"} + contract?(address_hash) -> + {:error, "This address isn't EOA"} match?({:restricted_access, true}, AccessHelper.restricted_access?(to_string(address_hash), %{})) -> {:error, "This address has restricted access"} @@ -42,15 +42,11 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do end end - defp is_contract(%Explorer.Chain.Hash{} = address_hash) do - query = - from( - token in Token, - where: token.contract_address_hash == ^address_hash - ) - - contract_addresses = Repo.all(query) - List.first(contract_addresses) + defp contract?(%Explorer.Chain.Hash{} = address_hash) do + case hash_to_address(address_hash) do + {:error, :not_found} -> false + {:ok, address} -> Address.smart_contract?(address) + end end defp format_address(address_hash_string) do diff --git a/apps/explorer/lib/explorer/account/notifier/notify.ex b/apps/explorer/lib/explorer/account/notifier/notify.ex index 70e633be8da8..3ad9e041c297 100644 --- a/apps/explorer/lib/explorer/account/notifier/notify.ex +++ b/apps/explorer/lib/explorer/account/notifier/notify.ex @@ -55,7 +55,8 @@ defmodule Explorer.Account.Notifier.Notify do defp notify_watchlists(nil), do: nil defp notify_watchlist(%WatchlistAddress{} = address, summary, direction) do - case ForbiddenAddress.check(address.address_hash) do + case !WatchlistNotification.limit_reached_for_watchlist_id?(address.watchlist_id) && + ForbiddenAddress.check(address.address_hash) do {:ok, _address_hash} -> with %WatchlistNotification{} = notification <- build_watchlist_notification( @@ -74,6 +75,9 @@ defmodule Explorer.Account.Notifier.Notify do {:error, _message} -> nil + + false -> + nil end end @@ -106,9 +110,6 @@ defmodule Explorer.Account.Notifier.Notify do Logger.info("--- email delivery response: FAILED", fetcher: :account) Logger.info(error, fetcher: :account) end - else - Logger.info("--- email delivery response: FAILED", fetcher: :account) - Logger.info("Email is not composed (is nil)", fetcher: :account) end end @@ -116,9 +117,10 @@ defmodule Explorer.Account.Notifier.Notify do direction = :incoming || :outgoing """ def build_watchlist_notification(%Explorer.Account.WatchlistAddress{} = address, summary, direction) do - if is_watched(address, summary, direction) do + if watched?(address, summary, direction) do %WatchlistNotification{ watchlist_address_id: address.id, + watchlist_id: address.watchlist_id, transaction_hash: summary.transaction_hash, from_address_hash: summary.from_address_hash, to_address_hash: summary.to_address_hash, @@ -138,7 +140,8 @@ defmodule Explorer.Account.Notifier.Notify do end end - defp is_watched(%WatchlistAddress{} = address, %{type: type}, direction) do + # credo:disable-for-next-line + defp watched?(%WatchlistAddress{} = address, %{type: type}, direction) do case {type, direction} do {"COIN", :incoming} -> address.watch_coin_input {"COIN", :outgoing} -> address.watch_coin_output @@ -148,6 +151,8 @@ defmodule Explorer.Account.Notifier.Notify do {"ERC-721", :outgoing} -> address.watch_erc_721_output {"ERC-1155", :incoming} -> address.watch_erc_1155_input {"ERC-1155", :outgoing} -> address.watch_erc_1155_output + {"ERC-404", :incoming} -> address.watch_erc_404_input + {"ERC-404", :outgoing} -> address.watch_erc_404_output end end diff --git a/apps/explorer/lib/explorer/account/notifier/summary.ex b/apps/explorer/lib/explorer/account/notifier/summary.ex index db39423bb48c..3364dfbb3744 100644 --- a/apps/explorer/lib/explorer/account/notifier/summary.ex +++ b/apps/explorer/lib/explorer/account/notifier/summary.ex @@ -8,7 +8,7 @@ defmodule Explorer.Account.Notifier.Summary do alias Explorer alias Explorer.Account.Notifier.Summary alias Explorer.{Chain, Repo} - alias Explorer.Chain.Wei + alias Explorer.Chain.{Transaction, Wei} defstruct [ :transaction_hash, @@ -132,7 +132,7 @@ defmodule Explorer.Account.Notifier.Summary do from_address_hash: transfer.from_address_hash, to_address_hash: transfer.to_address_hash, block_number: transfer.block_number, - subject: to_string(List.first(transfer.token_ids)), + subject: to_string(transfer.token_ids && List.first(transfer.token_ids)), tx_fee: fee(transaction), name: transfer.token.name, type: transfer.token.type @@ -151,6 +151,22 @@ defmodule Explorer.Account.Notifier.Summary do name: transfer.token.name, type: transfer.token.type } + + "ERC-404" -> + token_ids_string = token_ids(transfer) + + %Summary{ + amount: amount(transfer), + transaction_hash: transaction.hash, + method: method(transfer), + from_address_hash: transfer.from_address_hash, + to_address_hash: transfer.to_address_hash, + block_number: transfer.block_number, + subject: if(token_ids_string == "", do: transfer.token.type, else: token_ids_string), + tx_fee: fee(transaction), + name: transfer.token.name, + type: transfer.token.type + } end end @@ -191,6 +207,8 @@ defmodule Explorer.Account.Notifier.Summary do ) end + def token_ids(%Chain.TokenTransfer{token_ids: nil}), do: "" + def token_ids(%Chain.TokenTransfer{token_ids: token_ids}) do Enum.map_join(token_ids, ", ", fn id -> to_string(id) end) end @@ -203,7 +221,7 @@ defmodule Explorer.Account.Notifier.Summary do def type(%Chain.InternalTransaction{}), do: :coin def fee(%Chain.Transaction{} = transaction) do - {_, fee} = Chain.fee(transaction, :gwei) + {_, fee} = Transaction.fee(transaction, :gwei) fee end diff --git a/apps/explorer/lib/explorer/account/public_tags_request.ex b/apps/explorer/lib/explorer/account/public_tags_request.ex index eb989e5e7611..8cdf5e56fbda 100644 --- a/apps/explorer/lib/explorer/account/public_tags_request.ex +++ b/apps/explorer/lib/explorer/account/public_tags_request.ex @@ -19,21 +19,21 @@ defmodule Explorer.Account.PublicTagsRequest do @max_tags_per_request 2 @max_tag_length 35 - schema("account_public_tags_requests") do + typed_schema "account_public_tags_requests" do field(:company, :string) field(:website, :string) - field(:tags, :string) - field(:addresses, {:array, Hash.Address}) + field(:tags, :string, null: false) + field(:addresses, {:array, Hash.Address}, null: false) field(:description, :string) - field(:additional_comment, :string) - field(:request_type, :string) - field(:is_owner, :boolean, default: true) + field(:additional_comment, :string, null: false) + field(:request_type, :string, null: false) + field(:is_owner, :boolean, default: true, null: false) field(:remove_reason, :string) field(:request_id, :string) - field(:full_name, Explorer.Encrypted.Binary) - field(:email, Explorer.Encrypted.Binary) + field(:full_name, Explorer.Encrypted.Binary, null: false) + field(:email, Explorer.Encrypted.Binary, null: false) - belongs_to(:identity, Identity) + belongs_to(:identity, Identity, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/account/tag_address.ex b/apps/explorer/lib/explorer/account/tag_address.ex index 5d38db529a6d..6750e8a47f21 100644 --- a/apps/explorer/lib/explorer/account/tag_address.ex +++ b/apps/explorer/lib/explorer/account/tag_address.ex @@ -14,12 +14,12 @@ defmodule Explorer.Account.TagAddress do import Explorer.Chain, only: [hash_to_lower_case_string: 1] - schema "account_tag_addresses" do - field(:address_hash_hash, Cloak.Ecto.SHA256) - field(:name, Explorer.Encrypted.Binary) - field(:address_hash, Explorer.Encrypted.AddressHash) + typed_schema "account_tag_addresses" do + field(:address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:name, Explorer.Encrypted.Binary, null: false) + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) - belongs_to(:identity, Identity) + belongs_to(:identity, Identity, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/account/tag_transaction.ex b/apps/explorer/lib/explorer/account/tag_transaction.ex index b821fffcf9a0..f2c51672710d 100644 --- a/apps/explorer/lib/explorer/account/tag_transaction.ex +++ b/apps/explorer/lib/explorer/account/tag_transaction.ex @@ -12,12 +12,12 @@ defmodule Explorer.Account.TagTransaction do alias Explorer.{Chain, PagingOptions, Repo} import Explorer.Chain, only: [hash_to_lower_case_string: 1] - schema "account_tag_transactions" do - field(:tx_hash_hash, Cloak.Ecto.SHA256) - field(:name, Explorer.Encrypted.Binary) - field(:tx_hash, Explorer.Encrypted.TransactionHash) + typed_schema "account_tag_transactions" do + field(:tx_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:name, Explorer.Encrypted.Binary, null: false) + field(:tx_hash, Explorer.Encrypted.TransactionHash, null: false) - belongs_to(:identity, Identity) + belongs_to(:identity, Identity, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/account/watchlist.ex b/apps/explorer/lib/explorer/account/watchlist.ex index cd6998b83f77..0130ac6fcf6d 100644 --- a/apps/explorer/lib/explorer/account/watchlist.ex +++ b/apps/explorer/lib/explorer/account/watchlist.ex @@ -10,8 +10,8 @@ defmodule Explorer.Account.Watchlist do alias Explorer.Account.{Identity, WatchlistAddress} @derive {Jason.Encoder, only: [:name, :watchlist_addresses]} - schema "account_watchlists" do - field(:name, :string) + typed_schema "account_watchlists" do + field(:name, :string, null: false) belongs_to(:identity, Identity) has_many(:watchlist_addresses, WatchlistAddress) diff --git a/apps/explorer/lib/explorer/account/watchlist_address.ex b/apps/explorer/lib/explorer/account/watchlist_address.ex index e488ac758e13..7c5131609abe 100644 --- a/apps/explorer/lib/explorer/account/watchlist_address.ex +++ b/apps/explorer/lib/explorer/account/watchlist_address.ex @@ -15,22 +15,24 @@ defmodule Explorer.Account.WatchlistAddress do import Explorer.Chain, only: [hash_to_lower_case_string: 1] - schema "account_watchlist_addresses" do - field(:address_hash_hash, Cloak.Ecto.SHA256) - field(:name, Explorer.Encrypted.Binary) - field(:address_hash, Explorer.Encrypted.AddressHash) - - belongs_to(:watchlist, Watchlist) - - field(:watch_coin_input, :boolean, default: true) - field(:watch_coin_output, :boolean, default: true) - field(:watch_erc_20_input, :boolean, default: true) - field(:watch_erc_20_output, :boolean, default: true) - field(:watch_erc_721_input, :boolean, default: true) - field(:watch_erc_721_output, :boolean, default: true) - field(:watch_erc_1155_input, :boolean, default: true) - field(:watch_erc_1155_output, :boolean, default: true) - field(:notify_email, :boolean, default: true) + typed_schema "account_watchlist_addresses" do + field(:address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:name, Explorer.Encrypted.Binary, null: false) + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) + + belongs_to(:watchlist, Watchlist, null: false) + + field(:watch_coin_input, :boolean, default: true, null: false) + field(:watch_coin_output, :boolean, default: true, null: false) + field(:watch_erc_20_input, :boolean, default: true, null: false) + field(:watch_erc_20_output, :boolean, default: true, null: false) + field(:watch_erc_721_input, :boolean, default: true, null: false) + field(:watch_erc_721_output, :boolean, default: true, null: false) + field(:watch_erc_1155_input, :boolean, default: true, null: false) + field(:watch_erc_1155_output, :boolean, default: true, null: false) + field(:watch_erc_404_input, :boolean, default: true, null: false) + field(:watch_erc_404_output, :boolean, default: true, null: false) + field(:notify_email, :boolean, default: true, null: false) field(:notify_epns, :boolean) field(:notify_feed, :boolean) field(:notify_inapp, :boolean) @@ -43,7 +45,7 @@ defmodule Explorer.Account.WatchlistAddress do timestamps() end - @attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a + @attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output watch_erc_404_input watch_erc_404_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a def changeset do %__MODULE__{} diff --git a/apps/explorer/lib/explorer/account/watchlist_notification.ex b/apps/explorer/lib/explorer/account/watchlist_notification.ex index 935e53218780..fe28c7efac9c 100644 --- a/apps/explorer/lib/explorer/account/watchlist_notification.ex +++ b/apps/explorer/lib/explorer/account/watchlist_notification.ex @@ -1,6 +1,6 @@ defmodule Explorer.Account.WatchlistNotification do @moduledoc """ - Stored notification about event + Stored notification about event related to WatchlistAddress """ @@ -9,29 +9,31 @@ defmodule Explorer.Account.WatchlistNotification do import Ecto.Changeset import Explorer.Chain, only: [hash_to_lower_case_string: 1] - alias Explorer.Account.WatchlistAddress + alias Explorer.Repo + alias Explorer.Account.{Watchlist, WatchlistAddress} - schema "account_watchlist_notifications" do - field(:amount, :decimal) - field(:block_number, :integer) - field(:direction, :string) - field(:method, :string) - field(:tx_fee, :decimal) - field(:type, :string) - field(:viewed_at, :integer) - field(:name, Explorer.Encrypted.Binary) + typed_schema "account_watchlist_notifications" do + field(:amount, :decimal, null: false) + field(:block_number, :integer, null: false) + field(:direction, :string, null: false) + field(:method, :string, null: false) + field(:tx_fee, :decimal, null: false) + field(:type, :string, null: false) + field(:viewed_at, :integer, null: false) + field(:name, Explorer.Encrypted.Binary, null: false) field(:subject, Explorer.Encrypted.Binary) - field(:subject_hash, Cloak.Ecto.SHA256) + field(:subject_hash, Cloak.Ecto.SHA256) :: binary() | nil belongs_to(:watchlist_address, WatchlistAddress) + belongs_to(:watchlist, Watchlist) field(:from_address_hash, Explorer.Encrypted.AddressHash) field(:to_address_hash, Explorer.Encrypted.AddressHash) field(:transaction_hash, Explorer.Encrypted.TransactionHash) - field(:from_address_hash_hash, Cloak.Ecto.SHA256) - field(:to_address_hash_hash, Cloak.Ecto.SHA256) - field(:transaction_hash_hash, Cloak.Ecto.SHA256) + field(:from_address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:to_address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:transaction_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil timestamps() end @@ -62,4 +64,23 @@ defmodule Explorer.Account.WatchlistNotification do |> put_change(:transaction_hash_hash, hash_to_lower_case_string(get_field(changeset, :transaction_hash))) |> put_change(:subject_hash, get_field(changeset, :subject)) end + + @doc """ + Check if amount of watchlist notifications for the last 30 days is less than ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS + """ + @spec limit_reached_for_watchlist_id?(integer) :: boolean + def limit_reached_for_watchlist_id?(watchlist_id) do + __MODULE__ + |> where( + [wn], + wn.watchlist_id == ^watchlist_id and + fragment("NOW() - ? at time zone 'UTC' <= interval '30 days'", wn.inserted_at) + ) + |> limit(^watchlist_notification_30_days_limit()) + |> Repo.account_repo().aggregate(:count) == watchlist_notification_30_days_limit() + end + + defp watchlist_notification_30_days_limit do + Application.get_env(:explorer, Explorer.Account)[:notifications_limit_for_30_days] + end end diff --git a/apps/explorer/lib/explorer/accounts/user.ex b/apps/explorer/lib/explorer/accounts/user.ex index fd797d0b9532..20291b95c651 100644 --- a/apps/explorer/lib/explorer/accounts/user.ex +++ b/apps/explorer/lib/explorer/accounts/user.ex @@ -16,13 +16,7 @@ defmodule Explorer.Accounts.User do * `:password_hash` - Encrypted password * `:contacts` - List of `t:UserContact.t/0` """ - @type t :: %User{ - username: String.t(), - password_hash: String.t(), - contacts: [UserContact.t()] - } - - schema "users" do + typed_schema "users" do field(:username, :string) field(:password, :string, virtual: true) field(:password_hash, :string) diff --git a/apps/explorer/lib/explorer/accounts/user/authenticate.ex b/apps/explorer/lib/explorer/accounts/user/authenticate.ex index 08626c18b0cb..ea127d1a05ab 100644 --- a/apps/explorer/lib/explorer/accounts/user/authenticate.ex +++ b/apps/explorer/lib/explorer/accounts/user/authenticate.ex @@ -7,9 +7,9 @@ defmodule Explorer.Accounts.User.Authenticate do import Ecto.Changeset - embedded_schema do - field(:username, :string) - field(:password, :string) + typed_embedded_schema do + field(:username, :string, null: false) + field(:password, :string, null: false) end @required_attrs ~w(password username)a diff --git a/apps/explorer/lib/explorer/accounts/user/registration.ex b/apps/explorer/lib/explorer/accounts/user/registration.ex index 360e6bbb5e00..b62dd1c25e40 100644 --- a/apps/explorer/lib/explorer/accounts/user/registration.ex +++ b/apps/explorer/lib/explorer/accounts/user/registration.ex @@ -9,13 +9,11 @@ defmodule Explorer.Accounts.User.Registration do alias Explorer.Accounts.User.Registration - @type t :: %__MODULE__{} - - embedded_schema do - field(:username, :string) - field(:email, :string) - field(:password, :string) - field(:password_confirmation, :string) + typed_embedded_schema do + field(:username, :string, null: false) + field(:email, :string, null: false) + field(:password, :string, null: false) + field(:password_confirmation, :string, null: false) end @fields ~w(email password password_confirmation username)a diff --git a/apps/explorer/lib/explorer/accounts/user_contact.ex b/apps/explorer/lib/explorer/accounts/user_contact.ex index 5d6117ef998b..5b53692f38e1 100644 --- a/apps/explorer/lib/explorer/accounts/user_contact.ex +++ b/apps/explorer/lib/explorer/accounts/user_contact.ex @@ -19,17 +19,10 @@ defmodule Explorer.Accounts.UserContact do * `:verified` - Flag indicating if email contact has been verified * `:user` - owning `t:User.t/0` """ - @type t :: %UserContact{ - email: String.t(), - primary: boolean(), - verified: boolean(), - user: User.t() - } - - schema "user_contacts" do - field(:email, :string) - field(:primary, :boolean, default: false) - field(:verified, :boolean, default: false) + typed_schema "user_contacts" do + field(:email, :string, null: false) + field(:primary, :boolean, default: false, null: false) + field(:verified, :boolean, default: false, null: false) belongs_to(:user, User) diff --git a/apps/explorer/lib/explorer/admin/administrator.ex b/apps/explorer/lib/explorer/admin/administrator.ex index 20ec8d92b320..2f3109c10bc4 100644 --- a/apps/explorer/lib/explorer/admin/administrator.ex +++ b/apps/explorer/lib/explorer/admin/administrator.ex @@ -8,22 +8,15 @@ defmodule Explorer.Admin.Administrator do import Ecto.Changeset alias Explorer.Accounts.User - alias Explorer.Admin.Administrator - alias Explorer.Admin.Administrator.Role @typedoc """ * `:role` - Administrator's role determining permission level * `:user` - The `t:User.t/0` that is an admin * `:user_id` - User foreign key """ - @type t :: %Administrator{ - role: Role.t(), - user: User.t() | %Ecto.Association.NotLoaded{} - } - - schema "administrators" do - field(:role, :string) - belongs_to(:user, User) + typed_schema "administrators" do + field(:role, :string, null: false) + belongs_to(:user, User, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index f59e56903bc5..96876b008b9b 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -5,13 +5,14 @@ defmodule Explorer.Application do use Application - alias Explorer.{Admin, TokenTransferTokenIdMigration} + alias Explorer.Admin alias Explorer.Chain.Cache.{ Accounts, AddressesTabsCounters, AddressSum, AddressSumMinusBurnt, + BackgroundMigrations, Block, BlockNumber, Blocks, @@ -63,6 +64,7 @@ defmodule Explorer.Application do Accounts, AddressSum, AddressSumMinusBurnt, + BackgroundMigrations, Block, BlockNumber, Blocks, @@ -113,19 +115,28 @@ defmodule Explorer.Application do configure(Explorer.Counters.AddressTokenUsdSum), configure(Explorer.Counters.TokenHoldersCounter), configure(Explorer.Counters.TokenTransfersCounter), - configure(Explorer.Counters.BlockBurnedFeeCounter), + configure(Explorer.Counters.BlockBurntFeeCounter), configure(Explorer.Counters.BlockPriorityFeeCounter), configure(Explorer.Counters.AverageBlockTime), - configure(Explorer.Counters.Bridge), + configure(Explorer.Counters.LastOutputRootSizeCounter), + configure(Explorer.Counters.FreshPendingTransactionsCounter), + configure(Explorer.Counters.Transactions24hStats), configure(Explorer.Validator.MetadataProcessor), configure(Explorer.Tags.AddressTag.Cataloger), configure(MinMissingBlockNumber), - configure(TokenTransferTokenIdMigration.Supervisor), configure(Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand), configure(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand), configure(Explorer.TokenInstanceOwnerAddressMigration.Supervisor), sc_microservice_configure(Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand), - configure(Explorer.Chain.Cache.RootstockLockedBTC) + configure(Explorer.Chain.Cache.RootstockLockedBTC), + configure(Explorer.Chain.Cache.OptimismFinalizationPeriod), + configure(Explorer.Migrator.TransactionsDenormalization), + configure(Explorer.Migrator.AddressCurrentTokenBalanceTokenType), + configure(Explorer.Migrator.AddressTokenBalanceTokenType), + configure(Explorer.Migrator.SanitizeMissingBlockRanges), + configure(Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers), + configure(Explorer.Migrator.TokenTransferTokenType), + configure_chain_type_dependent_process(Explorer.Chain.Cache.StabilityValidatorsCounters, "stability") ] |> List.flatten() @@ -134,7 +145,19 @@ defmodule Explorer.Application do defp repos_by_chain_type do if Mix.env() == :test do - [Explorer.Repo.PolygonEdge, Explorer.Repo.PolygonZkevm, Explorer.Repo.RSK, Explorer.Repo.Suave] + [ + Explorer.Repo.Beacon, + Explorer.Repo.Optimism, + Explorer.Repo.PolygonEdge, + Explorer.Repo.PolygonZkevm, + Explorer.Repo.ZkSync, + Explorer.Repo.RSK, + Explorer.Repo.Shibarium, + Explorer.Repo.Suave, + Explorer.Repo.BridgedTokens, + Explorer.Repo.Filecoin, + Explorer.Repo.Stability + ] else [] end @@ -160,6 +183,14 @@ defmodule Explorer.Application do end end + defp configure_chain_type_dependent_process(process, chain_type) do + if Application.get_env(:explorer, :chain_type) == chain_type do + process + else + [] + end + end + defp sc_microservice_configure(process) do if Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:eth_bytecode_db?] do process diff --git a/apps/explorer/lib/explorer/application/constants.ex b/apps/explorer/lib/explorer/application/constants.ex index a7dea3535756..7dee1bbdae76 100644 --- a/apps/explorer/lib/explorer/application/constants.ex +++ b/apps/explorer/lib/explorer/application/constants.ex @@ -5,13 +5,15 @@ defmodule Explorer.Application.Constants do use Explorer.Schema alias Explorer.{Chain, Repo} + alias Explorer.Chain.Hash @keys_manager_contract_address_key "keys_manager_contract_address" + @last_processed_erc_721_token "token_instance_sanitizer_last_processed_erc_721_token" @primary_key false - schema "constants" do - field(:key, :string, primary_key: true) - field(:value, :string) + typed_schema "constants" do + field(:key, :string, primary_key: true, null: false) + field(:value, :string, null: false) timestamps() end @@ -43,4 +45,38 @@ defmodule Explorer.Application.Constants do def get_keys_manager_contract_address(options \\ []) do get_constant_by_key(@keys_manager_contract_address_key, options) end + + @doc """ + For usage in Indexer.Fetcher.TokenInstance.SanitizeERC721 + """ + @spec insert_last_processed_token_address_hash(Hash.Address.t()) :: Ecto.Schema.t() + def insert_last_processed_token_address_hash(address_hash) do + existing_value = Repo.get(__MODULE__, @last_processed_erc_721_token) + + if existing_value do + existing_value + |> changeset(%{value: to_string(address_hash)}) + |> Repo.update!() + else + %{key: @last_processed_erc_721_token, value: to_string(address_hash)} + |> changeset() + |> Repo.insert!() + end + end + + @doc """ + For usage in Indexer.Fetcher.TokenInstance.SanitizeERC721 + """ + @spec get_last_processed_token_address_hash(keyword()) :: nil | Explorer.Chain.Hash.t() + def get_last_processed_token_address_hash(options \\ []) do + result = get_constant_by_key(@last_processed_erc_721_token, options) + + case Chain.string_to_address_hash(result) do + {:ok, address_hash} -> + address_hash + + _ -> + nil + end + end end diff --git a/apps/explorer/lib/explorer/bloom_filter.ex b/apps/explorer/lib/explorer/bloom_filter.ex new file mode 100644 index 000000000000..a5b636207a9a --- /dev/null +++ b/apps/explorer/lib/explorer/bloom_filter.ex @@ -0,0 +1,78 @@ +defmodule Explorer.BloomFilter do + @moduledoc """ + Eth Bloom filter realization. Reference: https://github.com/NethermindEth/nethermind/blob/d61c78af6de2d0a89bd4efd6bfed62cb6b774f59/src/Nethermind/Nethermind.Core/Bloom.cs + """ + import Bitwise + + alias Explorer.BloomFilter + alias Explorer.Chain.Log + + @bloom_byte_length 256 + @bloom_bit_length 8 * @bloom_byte_length + + defstruct filter: <<0::2048>> + + @doc """ + Computes bloom filter from list of logs + """ + @spec logs_bloom([Log.t()]) :: <<_::2048>> + def logs_bloom(logs) do + logs + |> Enum.reduce(%BloomFilter{}, fn log, acc -> + topics = + Enum.reject( + [log.first_topic, log.second_topic, log.third_topic, log.fourth_topic], + &is_nil/1 + ) + + acc_new = + acc + |> add(log.address_hash.bytes) + + Enum.reduce(topics, acc_new, fn topic, acc -> add(acc, topic.bytes) end) + end) + |> Map.get(:filter) + end + + defp add(%BloomFilter{filter: filter} = bloom, element) do + {i1, i2, i3} = get_extract(element) + + new_filter = + filter + |> set_index(i1) + |> set_index(i2) + |> set_index(i3) + + %BloomFilter{bloom | filter: new_filter} + end + + defp hash_function(data), do: ExKeccak.hash_256(data) + + defp set_index(filter, index) do + byte_position = div(index, 8) + shift = rem(index, 8) + + byte = :binary.at(filter, byte_position) + value = set_bit(byte, shift) + + <> = filter + + <> + end + + defp set_bit(byte, bit) do + mask = 1 <<< (7 - bit) + + bor(byte, mask) + end + + defp get_extract(bytes) do + hash = hash_function(bytes) + + {get_index(hash, 0, 1), get_index(hash, 2, 3), get_index(hash, 4, 5)} + end + + defp get_index(bytes, index_1, index_2) do + @bloom_bit_length - 1 - rem((:binary.at(bytes, index_1) <<< 8) + :binary.at(bytes, index_2), 2048) + end +end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 498de42328c6..4085da40f5e6 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -26,7 +26,6 @@ defmodule Explorer.Chain do ] import EthereumJSONRPC, only: [integer_to_quantity: 1, fetch_block_internal_transactions: 2] - import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] require Logger @@ -34,6 +33,7 @@ defmodule Explorer.Chain do alias Ecto.{Changeset, Multi} alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction + alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Account.WatchlistAddress @@ -48,17 +48,17 @@ defmodule Explorer.Chain do Address.CurrentTokenBalance, Address.TokenBalance, Block, + BlockNumberHelper, CurrencyHelper, Data, DecompiledSmartContract, + DenormalizationHelper, Hash, Import, InternalTransaction, Log, PendingBlockOperation, - Search, SmartContract, - SmartContractAdditionalSource, Token, Token.Instance, TokenTransfer, @@ -67,10 +67,7 @@ defmodule Explorer.Chain do Withdrawal } - alias Explorer.Chain.Block.{EmissionReward, Reward} - alias Explorer.Chain.Cache.{ - Accounts, BlockNumber, Blocks, ContractsCounter, @@ -83,15 +80,15 @@ defmodule Explorer.Chain do } alias Explorer.Chain.Cache.Block, as: BlockCache + alias Explorer.Chain.Cache.Helper, as: CacheHelper alias Explorer.Chain.Cache.PendingBlockOperation, as: PendingBlockOperationCache alias Explorer.Chain.Fetcher.{CheckBytecodeMatchingOnDemand, LookUpSmartContractSourcesOnDemand} alias Explorer.Chain.Import.Runner alias Explorer.Chain.InternalTransaction.{CallType, Type} + alias Explorer.Chain.SmartContract.Proxy.EIP1167 alias Explorer.Market.MarketHistoryCache alias Explorer.{PagingOptions, Repo} - alias Explorer.SmartContract.Helper - alias Explorer.SmartContract.Solidity.Verifier alias Dataloader.Ecto, as: DataloaderEcto @@ -164,7 +161,7 @@ defmodule Explorer.Chain do """ @type necessity_by_association :: %{association => necessity} - @typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association} + @type necessity_by_association_option :: {:necessity_by_association, necessity_by_association} @type paging_options :: {:paging_options, PagingOptions.t()} @typep balance_by_day :: %{date: String.t(), value: Wei.t()} @type api? :: {:api?, true | false} @@ -206,7 +203,7 @@ defmodule Explorer.Chain do InternalTransaction |> InternalTransaction.where_nonpending_block() |> InternalTransaction.where_address_fields_match(hash, :to_address_hash) - |> InternalTransaction.where_block_number_in_period(from_block, to_block) + |> where_block_number_in_period(from_block, to_block) |> common_where_limit_order(paging_options) |> wrapped_union_subquery() @@ -214,7 +211,7 @@ defmodule Explorer.Chain do InternalTransaction |> InternalTransaction.where_nonpending_block() |> InternalTransaction.where_address_fields_match(hash, :from_address_hash) - |> InternalTransaction.where_block_number_in_period(from_block, to_block) + |> where_block_number_in_period(from_block, to_block) |> common_where_limit_order(paging_options) |> wrapped_union_subquery() @@ -222,7 +219,7 @@ defmodule Explorer.Chain do InternalTransaction |> InternalTransaction.where_nonpending_block() |> InternalTransaction.where_address_fields_match(hash, :created_contract_address_hash) - |> InternalTransaction.where_block_number_in_period(from_block, to_block) + |> where_block_number_in_period(from_block, to_block) |> common_where_limit_order(paging_options) |> wrapped_union_subquery() @@ -238,7 +235,7 @@ defmodule Explorer.Chain do InternalTransaction |> InternalTransaction.where_nonpending_block() |> InternalTransaction.where_address_fields_match(hash, direction) - |> InternalTransaction.where_block_number_in_period(from_block, to_block) + |> where_block_number_in_period(from_block, to_block) |> common_where_limit_order(paging_options) |> preload(:block) |> join_associations(necessity_by_association) @@ -266,234 +263,30 @@ defmodule Explorer.Chain do ) end - @doc """ - Fetches the transactions related to the address with the given hash, including - transactions that only have the address in the `token_transfers` related table - and rewards for block validation. - - This query is divided into multiple subqueries intentionally in order to - improve the listing performance. - - The `token_transfers` table tends to grow exponentially, and the query results - with a `transactions` `join` statement takes too long. - - To solve this the `transaction_hashes` are fetched in a separate query, and - paginated through the `block_number` already present in the `token_transfers` - table. - - ## Options - - * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is - `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the - `t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. - * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and - `:key` (a tuple of the lowest/oldest `{block_number, index}`) and. Results will be the transactions older than - the `block_number` and `index` that are passed. - - """ - @spec address_to_transactions_with_rewards(Hash.Address.t(), [paging_options | necessity_by_association_option]) :: - [ - Transaction.t() - ] - def address_to_transactions_with_rewards(address_hash, options \\ []) when is_list(options) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do - cond do - Keyword.get(options, :direction) == :from -> - address_to_transactions_without_rewards(address_hash, options) - - address_has_rewards?(address_hash) -> - address_with_rewards(address_hash, options, paging_options) - - true -> - address_to_transactions_without_rewards(address_hash, options) - end - else - address_to_transactions_without_rewards(address_hash, options) - end - end - - defp address_with_rewards(address_hash, options, paging_options) do - %{payout_key: block_miner_payout_address} = Reward.get_validator_payout_key_by_mining_from_db(address_hash, options) - - if block_miner_payout_address && address_hash == block_miner_payout_address do - transactions_with_rewards_results(address_hash, options, paging_options) - else - address_to_transactions_without_rewards(address_hash, options) - end - end - - defp transactions_with_rewards_results(address_hash, options, paging_options) do - blocks_range = address_to_transactions_tasks_range_of_blocks(address_hash, options) - - rewards_task = - Task.async(fn -> Reward.fetch_emission_rewards_tuples(address_hash, paging_options, blocks_range, options) end) - - [rewards_task | address_to_transactions_tasks(address_hash, options, true)] - |> wait_for_address_transactions() - |> Enum.sort_by(fn item -> - case item do - {%Reward{} = emission_reward, _} -> - {-emission_reward.block.number, 1} - - item -> - process_item(item) - end - end) - |> Enum.dedup_by(fn item -> - case item do - {%Reward{} = emission_reward, _} -> - {emission_reward.block_hash, emission_reward.address_hash, emission_reward.address_type} - - transaction -> - transaction.hash - end - end) - |> Enum.take(paging_options.page_size) - end - - defp process_item(item) do - block_number = if item.block_number, do: -item.block_number, else: 0 - index = if item.index, do: -item.index, else: 0 - {block_number, index} - end - - def address_to_transactions_without_rewards(address_hash, options, old_ui? \\ true) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - address_hash - |> address_to_transactions_tasks(options, old_ui?) - |> wait_for_address_transactions() - |> Enum.sort_by(&{&1.block_number, &1.index}, &>=/2) - |> Enum.dedup_by(& &1.hash) - |> Enum.take(paging_options.page_size) - end - def address_hashes_to_mined_transactions_without_rewards(address_hashes, options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) address_hashes |> address_hashes_to_mined_transactions_tasks(options) - |> wait_for_address_transactions() + |> Transaction.wait_for_address_transactions() |> Enum.sort_by(&{&1.block_number, &1.index}, &>=/2) |> Enum.dedup_by(& &1.hash) |> Enum.take(paging_options.page_size) end - defp address_to_transactions_tasks_query(options, only_mined? \\ false) do - from_block = from_block(options) - to_block = to_block(options) - - options - |> Keyword.get(:paging_options, @default_paging_options) - |> fetch_transactions(from_block, to_block, !only_mined?) - end - - defp transactions_block_numbers_at_address(address_hash, options) do - direction = Keyword.get(options, :direction) - - options - |> address_to_transactions_tasks_query() - |> Transaction.not_pending_transactions() - |> select([t], t.block_number) - |> Transaction.matching_address_queries_list(direction, address_hash) - end - - defp address_to_transactions_tasks(address_hash, options, old_ui?) do - direction = Keyword.get(options, :direction) - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - - options - |> address_to_transactions_tasks_query() - |> Transaction.not_dropped_or_replaced_transactions() - |> join_associations(necessity_by_association) - |> put_has_token_transfers_to_tx(old_ui?) - |> Transaction.matching_address_queries_list(direction, address_hash) - |> Enum.map(fn query -> Task.async(fn -> select_repo(options).all(query) end) end) - end - defp address_hashes_to_mined_transactions_tasks(address_hashes, options) do direction = Keyword.get(options, :direction) necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) options - |> address_to_transactions_tasks_query(true) + |> Transaction.address_to_transactions_tasks_query(true) |> Transaction.not_pending_transactions() |> join_associations(necessity_by_association) - |> put_has_token_transfers_to_tx(false) + |> Transaction.put_has_token_transfers_to_tx(false) |> Transaction.matching_address_queries_list(direction, address_hashes) |> Enum.map(fn query -> Task.async(fn -> select_repo(options).all(query) end) end) end - def address_to_transactions_tasks_range_of_blocks(address_hash, options) do - extremums_list = - address_hash - |> transactions_block_numbers_at_address(options) - |> Enum.map(fn query -> - extremum_query = - from( - q in subquery(query), - select: %{min_block_number: min(q.block_number), max_block_number: max(q.block_number)} - ) - - extremum_query - |> Repo.one!() - end) - - extremums_list - |> Enum.reduce(%{min_block_number: nil, max_block_number: 0}, fn %{ - min_block_number: min_number, - max_block_number: max_number - }, - extremums_result -> - current_min_number = Map.get(extremums_result, :min_block_number) - current_max_number = Map.get(extremums_result, :max_block_number) - - extremums_result - |> process_extremums_result_against_min_number(current_min_number, min_number) - |> process_extremums_result_against_max_number(current_max_number, max_number) - end) - end - - defp process_extremums_result_against_min_number(extremums_result, current_min_number, min_number) - when is_number(current_min_number) and - not (is_number(min_number) and min_number > 0 and min_number < current_min_number) do - extremums_result - end - - defp process_extremums_result_against_min_number(extremums_result, _current_min_number, min_number) do - extremums_result - |> Map.put(:min_block_number, min_number) - end - - defp process_extremums_result_against_max_number(extremums_result, current_max_number, max_number) - when is_number(max_number) and max_number > 0 and max_number > current_max_number do - extremums_result - |> Map.put(:max_block_number, max_number) - end - - defp process_extremums_result_against_max_number(extremums_result, _current_max_number, _max_number) do - extremums_result - end - - defp wait_for_address_transactions(tasks) do - tasks - |> Task.yield_many(:timer.seconds(20)) - |> Enum.flat_map(fn {_task, res} -> - case res do - {:ok, result} -> - result - - {:exit, reason} -> - raise "Query fetching address transactions terminated: #{inspect(reason)}" - - nil -> - raise "Query fetching address transactions timed out." - end - end) - end - @spec address_hash_to_token_transfers(Hash.Address.t(), Keyword.t()) :: [Transaction.t()] def address_hash_to_token_transfers(address_hash, options \\ []) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) @@ -502,7 +295,7 @@ defmodule Explorer.Chain do direction |> Transaction.transactions_with_token_transfers_direction(address_hash) |> Transaction.preload_token_transfers(address_hash) - |> handle_paging_options(paging_options) + |> Transaction.handle_paging_options(paging_options) |> Repo.all() end @@ -514,9 +307,8 @@ defmodule Explorer.Chain do necessity_by_association = Keyword.get(options, :necessity_by_association) direction - |> TokenTransfer.token_transfers_by_address_hash(address_hash, filters) + |> TokenTransfer.token_transfers_by_address_hash(address_hash, filters, paging_options) |> join_associations(necessity_by_association) - |> TokenTransfer.handle_paging_options(paging_options) |> select_repo(options).all() end @@ -560,15 +352,26 @@ defmodule Explorer.Chain do to_block = to_block(options) base = - from(log in Log, - order_by: [desc: log.block_number, desc: log.index], - where: log.address_hash == ^address_hash, - limit: ^paging_options.page_size, - select: log, - inner_join: block in Block, - on: block.hash == log.block_hash, - where: block.consensus == true - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from(log in Log, + order_by: [desc: log.block_number, desc: log.index], + where: log.address_hash == ^address_hash, + limit: ^paging_options.page_size, + select: log, + inner_join: transaction in assoc(log, :transaction), + where: transaction.block_consensus == true + ) + else + from(log in Log, + order_by: [desc: log.block_number, desc: log.index], + where: log.address_hash == ^address_hash, + limit: ^paging_options.page_size, + select: log, + inner_join: block in Block, + on: block.hash == log.block_hash, + where: block.consensus == true + ) + end preloaded_query = if csv_export? do @@ -637,7 +440,7 @@ defmodule Explorer.Chain do address_hash |> Transaction.transactions_with_token_transfers(token_hash) |> Transaction.preload_token_transfers(address_hash) - |> handle_paging_options(paging_options) + |> Transaction.handle_paging_options(paging_options) |> Repo.all() end @@ -671,115 +474,6 @@ defmodule Explorer.Chain do Repo.aggregate(Block, :count, :hash) end - @doc """ - Reward for mining a block. - - The block reward is the sum of the following: - - * Sum of the transaction fees (gas_used * gas_price) for the block - * A static reward for miner (this value may change during the life of the chain) - * The reward for uncle blocks (1/32 * static_reward * number_of_uncles) - - *NOTE* - - Uncles are not currently accounted for. - """ - @spec block_reward(Block.block_number()) :: Wei.t() - def block_reward(block_number) do - block_hash = - Block - |> where([block], block.number == ^block_number and block.consensus == true) - |> select([block], block.hash) - |> Repo.one!() - - case Repo.one!( - from(reward in Reward, - where: reward.block_hash == ^block_hash, - select: %Wei{ - value: coalesce(sum(reward.reward), 0) - } - ) - ) do - %Wei{ - value: %Decimal{coef: 0} - } -> - Repo.one!( - from(block in Block, - left_join: transaction in assoc(block, :transactions), - inner_join: emission_reward in EmissionReward, - on: fragment("? <@ ?", block.number, emission_reward.block_range), - where: block.number == ^block_number and block.consensus == true, - group_by: [emission_reward.reward, block.hash], - select: %Wei{ - value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0) + emission_reward.reward - } - ) - ) - - other_value -> - other_value - end - end - - def txn_fees(transactions) do - Enum.reduce(transactions, Decimal.new(0), fn %{gas_used: gas_used, gas_price: gas_price}, acc -> - gas_used - |> Decimal.new() - |> Decimal.mult(gas_price_to_decimal(gas_price)) - |> Decimal.add(acc) - end) - end - - defp gas_price_to_decimal(%Wei{} = wei), do: wei.value - defp gas_price_to_decimal(gas_price), do: Decimal.new(gas_price) - - def burned_fees(transactions, base_fee_per_gas) do - burned_fee_counter = - transactions - |> Enum.reduce(Decimal.new(0), fn %{gas_used: gas_used}, acc -> - gas_used - |> Decimal.new() - |> Decimal.add(acc) - end) - - base_fee_per_gas && Wei.mult(base_fee_per_gas_to_wei(base_fee_per_gas), burned_fee_counter) - end - - defp base_fee_per_gas_to_wei(%Wei{} = wei), do: wei - defp base_fee_per_gas_to_wei(base_fee_per_gas), do: %Wei{value: Decimal.new(base_fee_per_gas)} - - @uncle_reward_coef 1 / 32 - def block_reward_by_parts(block, transactions) do - %{hash: block_hash, number: block_number} = block - base_fee_per_gas = Map.get(block, :base_fee_per_gas) - - txn_fees = txn_fees(transactions) - - static_reward = - Repo.one( - from( - er in EmissionReward, - where: fragment("int8range(?, ?) <@ ?", ^block_number, ^(block_number + 1), er.block_range), - select: er.reward - ) - ) || %Wei{value: Decimal.new(0)} - - has_uncles? = is_list(block.uncles) and not Enum.empty?(block.uncles) - - burned_fees = burned_fees(transactions, base_fee_per_gas) - uncle_reward = (has_uncles? && Wei.mult(static_reward, Decimal.from_float(@uncle_reward_coef))) || nil - - %{ - block_number: block_number, - block_hash: block_hash, - miner_hash: block.miner_hash, - static_reward: static_reward, - txn_fees: %Wei{value: txn_fees}, - burned_fees: burned_fees || %Wei{value: Decimal.new(0)}, - uncle_reward: uncle_reward || %Wei{value: Decimal.new(0)} - } - end - @doc """ The `t:Explorer.Chain.Wei.t/0` paid to the miners of the `t:Explorer.Chain.Block.t/0`s with `hash` `Explorer.Chain.Hash.Full.t/0` by the signers of the transactions in those blocks to cover the gas fee @@ -788,17 +482,34 @@ defmodule Explorer.Chain do @spec gas_payment_by_block_hash([Hash.Full.t()]) :: %{Hash.Full.t() => Wei.t()} def gas_payment_by_block_hash(block_hashes) when is_list(block_hashes) do query = - from( - block in Block, - left_join: transaction in assoc(block, :transactions), - where: block.hash in ^block_hashes and block.consensus == true, - group_by: block.hash, - select: {block.hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + transaction in Transaction, + where: transaction.block_hash in ^block_hashes and transaction.block_consensus == true, + group_by: transaction.block_hash, + select: {transaction.block_hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} + ) + else + from( + block in Block, + left_join: transaction in assoc(block, :transactions), + where: block.hash in ^block_hashes and block.consensus == true, + group_by: block.hash, + select: {block.hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} + ) + end - query - |> Repo.all() - |> Enum.into(%{}) + initial_gas_payments = + block_hashes + |> Enum.map(&{&1, %Wei{value: Decimal.new(0)}}) + |> Enum.into(%{}) + + existing_data = + query + |> Repo.all() + |> Enum.into(%{}) + + Map.merge(initial_gas_payments, existing_data) end def timestamp_by_block_hash(block_hashes) when is_list(block_hashes) do @@ -833,14 +544,16 @@ defmodule Explorer.Chain do ] def block_to_transactions(block_hash, options \\ [], old_ui? \\ true) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + type_filter = Keyword.get(options, :type) options |> Keyword.get(:paging_options, @default_paging_options) |> fetch_transactions_in_ascending_order_by_index() |> join(:inner, [transaction], block in assoc(transaction, :block)) |> where([_, block], block.hash == ^block_hash) + |> apply_filter_by_tx_type_to_transactions(type_filter) |> join_associations(necessity_by_association) - |> put_has_token_transfers_to_tx(old_ui?) + |> Transaction.put_has_token_transfers_to_tx(old_ui?) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() |> select_repo(options).all() |> (&if(old_ui?, @@ -860,7 +573,7 @@ defmodule Explorer.Chain do |> fetch_transactions_in_descending_order_by_block_and_index() |> where(execution_node_hash: ^execution_node_hash) |> join_associations(necessity_by_association) - |> put_has_token_transfers_to_tx(false) + |> Transaction.put_has_token_transfers_to_tx(false) |> (& &1).() |> select_repo(options).all() |> (&Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_necessity_by_association, options) end)).() @@ -1065,6 +778,33 @@ defmodule Explorer.Chain do end end + defp set_address_decompiled(repo, address_hash) do + query = + from( + address in Address, + where: address.hash == ^address_hash + ) + + case repo.update_all(query, set: [decompiled: true]) do + {1, _} -> {:ok, []} + _ -> {:error, "There was an error annotating that the address has been decompiled."} + end + end + + @spec verified_contracts_top(non_neg_integer()) :: [Hash.Address.t()] + def verified_contracts_top(limit) do + query = + from(contract in SmartContract, + inner_join: address in Address, + on: contract.address_hash == address.hash, + order_by: [desc: address.transactions_count], + limit: ^limit, + select: contract.address_hash + ) + + Repo.all(query) + end + @doc """ Converts the `Explorer.Chain.Data.t:t/0` to `iodata` representation that can be written to users efficiently. @@ -1089,54 +829,6 @@ defmodule Explorer.Chain do Data.to_iodata(data) end - @doc """ - The fee a `transaction` paid for the `t:Explorer.Transaction.t/0` `gas` - - If the transaction is pending, then the fee will be a range of `unit` - - iex> Explorer.Chain.fee( - ...> %Explorer.Chain.Transaction{ - ...> gas: Decimal.new(3), - ...> gas_price: %Explorer.Chain.Wei{value: Decimal.new(2)}, - ...> gas_used: nil - ...> }, - ...> :wei - ...> ) - {:maximum, Decimal.new(6)} - - If the transaction has been confirmed in block, then the fee will be the actual fee paid in `unit` for the `gas_used` - in the `transaction`. - - iex> Explorer.Chain.fee( - ...> %Explorer.Chain.Transaction{ - ...> gas: Decimal.new(3), - ...> gas_price: %Explorer.Chain.Wei{value: Decimal.new(2)}, - ...> gas_used: Decimal.new(2) - ...> }, - ...> :wei - ...> ) - {:actual, Decimal.new(4)} - - """ - @spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t()} | {:actual, Decimal.t()} - def fee(%Transaction{gas: gas, gas_price: gas_price, gas_used: nil}, unit) do - fee = - gas_price - |> Wei.to(unit) - |> Decimal.mult(gas) - - {:maximum, fee} - end - - def fee(%Transaction{gas_price: gas_price, gas_used: gas_used}, unit) do - fee = - gas_price - |> Wei.to(unit) - |> Decimal.mult(gas_used) - - {:actual, fee} - end - @doc """ Checks to see if the chain is down indexing based on the transaction from the oldest block and the pending operation @@ -1254,10 +946,10 @@ defmodule Explorer.Chain do Optionally it also accepts a boolean to fetch the `has_decompiled_code?` virtual field or not """ - @spec hash_to_address(Hash.Address.t(), [necessity_by_association_option | api?], boolean()) :: + @spec hash_to_address(Hash.Address.t() | binary(), [necessity_by_association_option | api?], boolean()) :: {:ok, Address.t()} | {:error, :not_found} def hash_to_address( - %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, + hash, options \\ [ necessity_by_association: %{ :contracts_creation_internal_transaction => :optional, @@ -1267,7 +959,7 @@ defmodule Explorer.Chain do :contracts_creation_transaction => :optional } ], - query_decompiled_code_flag \\ true + query_decompiled_code_flag \\ false ) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) @@ -1289,7 +981,7 @@ defmodule Explorer.Chain do if smart_contract do address_result else - compose_smart_contract(address_result, hash, options) + SmartContract.compose_smart_contract(address_result, hash, options) end _ -> @@ -1303,27 +995,6 @@ defmodule Explorer.Chain do end end - defp compose_smart_contract(address_result, hash, options) do - address_verified_twin_contract = - get_minimal_proxy_template(hash, options) || - get_address_verified_twin_contract(hash, options).verified_contract - - if address_verified_twin_contract do - address_verified_twin_contract_updated = - address_verified_twin_contract - |> Map.put(:address_hash, hash) - |> Map.put(:metadata_from_verified_twin, true) - |> Map.put(:implementation_address_hash, nil) - |> Map.put(:implementation_name, nil) - |> Map.put(:implementation_fetched_at, nil) - - address_result - |> Map.put(:smart_contract, address_verified_twin_contract_updated) - else - address_result - end - end - def decompiled_code(address_hash, version) do query = from(contract in DecompiledSmartContract, @@ -1421,11 +1092,13 @@ defmodule Explorer.Chain do @doc """ Converts list of `t:Explorer.Chain.Address.t/0` `hash` to the `t:Explorer.Chain.Address.t/0` with that `hash`. - Returns `[%Explorer.Chain.Address{}]}` if found + Returns `[%Explorer.Chain.Address{}]` if found """ - @spec hashes_to_addresses([Hash.Address.t()]) :: [Address.t()] - def hashes_to_addresses(hashes) when is_list(hashes) do + @spec hashes_to_addresses([Hash.Address.t()], [necessity_by_association_option | api?]) :: [Address.t()] + def hashes_to_addresses(hashes, options \\ []) when is_list(hashes) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + query = from( address in Address, @@ -1434,7 +1107,9 @@ defmodule Explorer.Chain do order_by: fragment("array_position(?, ?)", type(^hashes, {:array, Hash.Address}), address.hash) ) - Repo.all(query) + query + |> join_associations(necessity_by_association) + |> select_repo(options).all() end @doc """ @@ -1460,7 +1135,7 @@ defmodule Explorer.Chain do options |> Keyword.get(:necessity_by_association, %{}) |> Map.merge(%{ - smart_contract_additional_sources: :optional + [smart_contract: :smart_contract_additional_sources] => :optional }) query = @@ -1481,15 +1156,15 @@ defmodule Explorer.Chain do if smart_contract do CheckBytecodeMatchingOnDemand.trigger_check(address_result, smart_contract) LookUpSmartContractSourcesOnDemand.trigger_fetch(address_result, smart_contract) - check_and_update_constructor_args(address_result) + SmartContract.check_and_update_constructor_args(address_result) else LookUpSmartContractSourcesOnDemand.trigger_fetch(address_result, nil) address_verified_twin_contract = - get_minimal_proxy_template(hash, options) || - get_address_verified_twin_contract(hash, options).verified_contract + EIP1167.get_implementation_address(hash, options) || + SmartContract.get_address_verified_twin_contract(hash, options).verified_contract - add_twin_info_to_contract(address_result, address_verified_twin_contract, hash) + SmartContract.add_twin_info_to_contract(address_result, address_verified_twin_contract, hash) end _ -> @@ -1504,53 +1179,6 @@ defmodule Explorer.Chain do end end - defp check_and_update_constructor_args( - %SmartContract{address_hash: address_hash, constructor_arguments: nil, verified_via_sourcify: true} = - smart_contract - ) do - if args = Verifier.parse_constructor_arguments_for_sourcify_contract(address_hash, smart_contract.abi) do - smart_contract |> SmartContract.changeset(%{constructor_arguments: args}) |> Repo.update() - %SmartContract{smart_contract | constructor_arguments: args} - else - smart_contract - end - end - - defp check_and_update_constructor_args( - %Address{ - hash: address_hash, - contract_code: deployed_bytecode, - smart_contract: %SmartContract{constructor_arguments: nil, verified_via_sourcify: true} = smart_contract - } = address - ) do - if args = - Verifier.parse_constructor_arguments_for_sourcify_contract(address_hash, smart_contract.abi, deployed_bytecode) do - smart_contract |> SmartContract.changeset(%{constructor_arguments: args}) |> Repo.update() - %Address{address | smart_contract: %SmartContract{smart_contract | constructor_arguments: args}} - else - address - end - end - - defp check_and_update_constructor_args(other), do: other - - defp add_twin_info_to_contract(address_result, address_verified_twin_contract, _hash) - when is_nil(address_verified_twin_contract), - do: address_result - - defp add_twin_info_to_contract(address_result, address_verified_twin_contract, hash) do - address_verified_twin_contract_updated = - address_verified_twin_contract - |> Map.put(:address_hash, hash) - |> Map.put(:metadata_from_verified_twin, true) - |> Map.put(:implementation_address_hash, nil) - |> Map.put(:implementation_name, nil) - |> Map.put(:implementation_fetched_at, nil) - - address_result - |> Map.put(:smart_contract, address_verified_twin_contract_updated) - end - @spec find_decompiled_contract_address(Hash.Address.t()) :: {:ok, Address.t()} | {:error, :not_found} def find_decompiled_contract_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do query = @@ -1763,7 +1391,7 @@ defmodule Explorer.Chain do def hashes_to_transactions(hashes, options \\ []) when is_list(hashes) and is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - fetch_transactions() + Transaction.fetch_transactions() |> where([transaction], transaction.hash in ^hashes) |> join_associations(necessity_by_association) |> preload([{:token_transfers, [:token, :from_address, :to_address]}]) @@ -1786,7 +1414,7 @@ defmodule Explorer.Chain do If there are no blocks, the percentage is 0. iex> Explorer.Chain.indexed_ratio_blocks() - Decimal.new(0) + Decimal.new(1) """ @spec indexed_ratio_blocks() :: Decimal.t() @@ -1798,10 +1426,10 @@ defmodule Explorer.Chain do case {min_saved_block_number, max_saved_block_number} do {0, 0} -> - Decimal.new(0) + Decimal.new(1) _ -> - divisor = max_saved_block_number - min_blockchain_block_number + 1 + divisor = max_saved_block_number - min_blockchain_block_number - BlockNumberHelper.null_rounds_count() + 1 ratio = get_ratio(BlockCache.estimated_count(), divisor) @@ -1833,7 +1461,9 @@ defmodule Explorer.Chain do Decimal.new(0) _ -> - full_blocks_range = max_saved_block_number - min_blockchain_trace_block_number + 1 + full_blocks_range = + max_saved_block_number - min_blockchain_trace_block_number - BlockNumberHelper.null_rounds_count() + 1 + processed_int_txs_for_blocks_count = max(0, full_blocks_range - pbo_count) ratio = get_ratio(processed_int_txs_for_blocks_count, full_blocks_range) @@ -2030,110 +1660,25 @@ defmodule Explorer.Chain do |> Enum.into(%{}) end - @doc """ - Lists the top `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance and address hash. + def get_table_rows_total_count(module, options) do + table_name = module.__schema__(:source) - """ - @spec list_top_addresses :: [{Address.t(), non_neg_integer()}] - def list_top_addresses(options \\ []) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) + count = CacheHelper.estimated_count_from(table_name, options) - if is_nil(paging_options.key) do - paging_options.page_size - |> Accounts.take_enough() - |> case do - nil -> - get_addresses(options) - - accounts -> - Enum.map( - accounts, - &{&1, - if is_nil(&1.nonce) do - 0 - else - &1.nonce + 1 - end} - ) - end + if is_nil(count) do + select_repo(options).aggregate(module, :count, timeout: :infinity) else - fetch_top_addresses(options) + count end end - defp get_addresses(options) do - accounts_with_n = fetch_top_addresses(options) - - accounts_with_n - |> Enum.map(fn {address, _n} -> address end) - |> Accounts.update() - - accounts_with_n - end - - defp fetch_top_addresses(options) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - base_query = - from(a in Address, - where: a.fetched_coin_balance > ^0, - order_by: [desc: a.fetched_coin_balance, asc: a.hash], - preload: [:names, :smart_contract], - select: {a, fragment("coalesce(1 + ?, 0)", a.nonce)} - ) - - base_query - |> page_addresses(paging_options) - |> limit(^paging_options.page_size) - |> select_repo(options).all() - end - - @doc """ - Lists the top `t:Explorer.Chain.Token.t/0`'s'. - - """ - @spec list_top_tokens(String.t()) :: [{Token.t(), non_neg_integer()}] - def list_top_tokens(filter, options \\ []) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - token_type = Keyword.get(options, :token_type, nil) - sorting = Keyword.get(options, :sorting, []) - - fetch_top_tokens(filter, paging_options, token_type, sorting, options) - end - - defp fetch_top_tokens(filter, paging_options, token_type, sorting, options) do - base_query = Token.base_token_query(token_type, sorting) - - base_query_with_paging = - base_query - |> Token.page_tokens(paging_options, sorting) - |> limit(^paging_options.page_size) - - query = - if filter && filter !== "" do - case Search.prepare_search_term(filter) do - {:some, filter_term} -> - base_query_with_paging - |> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term)) - - _ -> - base_query_with_paging - end - else - base_query_with_paging - end - - query - |> select_repo(options).all() - end - - @doc """ - Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`. - """ - def stream_blocks_without_rewards(initial, reducer, limited? \\ false) when is_function(reducer, 2) do - Block.blocks_without_reward_query() - |> add_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) + @doc """ + Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`. + """ + def stream_blocks_without_rewards(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + Block.blocks_without_reward_query() + |> add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) end @doc """ @@ -2298,7 +1843,8 @@ defmodule Explorer.Chain do from( po in PendingBlockOperation, where: not is_nil(po.block_number), - select: po.block_number + select: po.block_number, + order_by: [desc: po.block_number] ) query @@ -2360,7 +1906,7 @@ defmodule Explorer.Chain do from(t in Transaction, where: not is_nil(t.block_hash) and not is_nil(t.created_contract_address_hash) and - is_nil(t.created_contract_code_indexed_at), + is_nil(t.created_contract_code_indexed_at) and t.status == ^1, select: ^fields ) @@ -2546,6 +2092,16 @@ defmodule Explorer.Chain do end end + @spec increment_last_fetched_counter(binary(), non_neg_integer()) :: {non_neg_integer(), nil} + def increment_last_fetched_counter(type, value) do + query = + from(counter in LastFetchedCounter, + where: counter.counter_type == ^type + ) + + Repo.update_all(query, [inc: [value: value]], timeout: :infinity) + end + @spec upsert_last_fetched_counter(map()) :: {:ok, LastFetchedCounter.t()} | {:error, Ecto.Changeset.t()} def upsert_last_fetched_counter(params) do changeset = LastFetchedCounter.changeset(%LastFetchedCounter{}, params) @@ -2564,7 +2120,11 @@ defmodule Explorer.Chain do select: last_fetched_counter.value ) - select_repo(options).one(query) || Decimal.new(0) + if options[:nullable] do + select_repo(options).one(query) + else + select_repo(options).one(query) || Decimal.new(0) + end end defp block_status({number, timestamp}) do @@ -2673,26 +2233,50 @@ defmodule Explorer.Chain do range_max = max(range_start, range_end) ordered_missing_query = - from(b in Block, - right_join: - missing_range in fragment( - """ - ( - SELECT distinct b1.number - FROM generate_series((?)::integer, (?)::integer) AS b1(number) - WHERE NOT EXISTS - (SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus) - ORDER BY b1.number DESC - ) - """, - ^range_min, - ^range_max - ), - on: b.number == missing_range.number, - select: missing_range.number, - order_by: missing_range.number, - distinct: missing_range.number - ) + if Application.get_env(:explorer, :chain_type) == "filecoin" do + from(b in Block, + right_join: + missing_range in fragment( + """ + ( + SELECT distinct b1.number + FROM generate_series((?)::integer, (?)::integer) AS b1(number) + WHERE NOT EXISTS + (SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus) + AND NOT EXISTS (SELECT 1 FROM null_round_heights nrh where nrh.height=b1.number) + ORDER BY b1.number DESC + ) + """, + ^range_min, + ^range_max + ), + on: b.number == missing_range.number, + select: missing_range.number, + order_by: missing_range.number, + distinct: missing_range.number + ) + else + from(b in Block, + right_join: + missing_range in fragment( + """ + ( + SELECT distinct b1.number + FROM generate_series((?)::integer, (?)::integer) AS b1(number) + WHERE NOT EXISTS + (SELECT 1 FROM blocks b2 WHERE b2.number=b1.number AND b2.consensus) + ORDER BY b1.number DESC + ) + """, + ^range_min, + ^range_max + ), + on: b.number == missing_range.number, + select: missing_range.number, + order_by: missing_range.number, + distinct: missing_range.number + ) + end missing_blocks = Repo.all(ordered_missing_query, timeout: :infinity) @@ -2830,13 +2414,13 @@ defmodule Explorer.Chain do DateTime.compare(timestamp, given_timestamp) == :eq do number else - number - 1 + BlockNumberHelper.previous_block_number(number) end :after -> if DateTime.compare(timestamp, given_timestamp) == :lt || DateTime.compare(timestamp, given_timestamp) == :eq do - number + 1 + BlockNumberHelper.next_block_number(number) else number end @@ -2968,12 +2552,12 @@ defmodule Explorer.Chain do options ) do paging_options - |> fetch_transactions() + |> Transaction.fetch_transactions() |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) |> apply_filter_by_method_id_to_transactions(method_id_filter) |> apply_filter_by_tx_type_to_transactions(type_filter) |> join_associations(necessity_by_association) - |> put_has_token_transfers_to_tx(old_ui?) + |> Transaction.put_has_token_transfers_to_tx(old_ui?) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() |> select_repo(options).all() |> (&if(old_ui?, @@ -3018,7 +2602,7 @@ defmodule Explorer.Chain do type_filter = Keyword.get(options, :type) Transaction - |> page_pending_transaction(paging_options) + |> Transaction.page_pending_transaction(paging_options) |> limit(^paging_options.page_size) |> pending_transactions_query() |> apply_filter_by_method_id_to_transactions(method_id_filter) @@ -3449,7 +3033,9 @@ defmodule Explorer.Chain do on: tx.created_contract_address_hash == a.hash, where: tx.created_contract_address_hash == ^address_hash, where: tx.status == ^1, - select: %{init: tx.input, created_contract_code: a.contract_code} + select: %{init: tx.input, created_contract_code: a.contract_code}, + order_by: [desc: tx.block_number], + limit: ^1 ) tx_input = @@ -3467,7 +3053,9 @@ defmodule Explorer.Chain do join: t in assoc(itx, :transaction), where: itx.created_contract_address_hash == ^address_hash, where: t.status == ^1, - select: %{init: itx.init, created_contract_code: itx.created_contract_code} + select: %{init: itx.init, created_contract_code: itx.created_contract_code}, + order_by: [desc: itx.block_number], + limit: ^1 ) res = creation_int_tx_query |> Repo.one() @@ -3569,441 +3157,6 @@ defmodule Explorer.Chain do end end - @doc """ - Inserts a `t:SmartContract.t/0`. - - As part of inserting a new smart contract, an additional record is inserted for - naming the address for reference. - """ - @spec create_smart_contract(map()) :: {:ok, SmartContract.t()} | {:error, Ecto.Changeset.t()} - def create_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do - new_contract = %SmartContract{} - - attrs = - attrs - |> Helper.add_contract_code_md5() - - smart_contract_changeset = - new_contract - |> SmartContract.changeset(attrs) - |> Changeset.put_change(:external_libraries, external_libraries) - - new_contract_additional_source = %SmartContractAdditionalSource{} - - smart_contract_additional_sources_changesets = - if secondary_sources do - secondary_sources - |> Enum.map(fn changeset -> - new_contract_additional_source - |> SmartContractAdditionalSource.changeset(changeset) - end) - else - [] - end - - address_hash = Changeset.get_field(smart_contract_changeset, :address_hash) - - # Enforce ShareLocks tables order (see docs: sharelocks.md) - insert_contract_query = - Multi.new() - |> Multi.run(:set_address_verified, fn repo, _ -> set_address_verified(repo, address_hash) end) - |> Multi.run(:clear_primary_address_names, fn repo, _ -> clear_primary_address_names(repo, address_hash) end) - |> Multi.insert(:smart_contract, smart_contract_changeset) - - insert_contract_query_with_additional_sources = - smart_contract_additional_sources_changesets - |> Enum.with_index() - |> Enum.reduce(insert_contract_query, fn {changeset, index}, multi -> - Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset) - end) - - insert_result = - insert_contract_query_with_additional_sources - |> Repo.transaction() - - create_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) - - case insert_result do - {:ok, %{smart_contract: smart_contract}} -> - {:ok, smart_contract} - - {:error, :smart_contract, changeset, _} -> - {:error, changeset} - - {:error, :set_address_verified, message, _} -> - {:error, message} - end - end - - @doc """ - Updates a `t:SmartContract.t/0`. - - Has the similar logic as create_smart_contract/1. - Used in cases when you need to update row in DB contains SmartContract, e.g. in case of changing - status `partially verified` to `fully verified` (re-verify). - """ - @spec update_smart_contract(map()) :: {:ok, SmartContract.t()} | {:error, Ecto.Changeset.t()} - def update_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do - address_hash = Map.get(attrs, :address_hash) - - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^address_hash - ) - - query_sources = - from( - source in SmartContractAdditionalSource, - where: source.address_hash == ^address_hash - ) - - _delete_sources = Repo.delete_all(query_sources) - - smart_contract = Repo.one(query) - - smart_contract_changeset = - smart_contract - |> SmartContract.changeset(attrs) - |> Changeset.put_change(:external_libraries, external_libraries) - - new_contract_additional_source = %SmartContractAdditionalSource{} - - smart_contract_additional_sources_changesets = - if secondary_sources do - secondary_sources - |> Enum.map(fn changeset -> - new_contract_additional_source - |> SmartContractAdditionalSource.changeset(changeset) - end) - else - [] - end - - # Enforce ShareLocks tables order (see docs: sharelocks.md) - insert_contract_query = - Multi.new() - |> Multi.run(:clear_primary_address_names, fn repo, _ -> clear_primary_address_names(repo, address_hash) end) - |> Multi.update(:smart_contract, smart_contract_changeset) - - insert_contract_query_with_additional_sources = - smart_contract_additional_sources_changesets - |> Enum.with_index() - |> Enum.reduce(insert_contract_query, fn {changeset, index}, multi -> - Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset) - end) - - insert_result = - insert_contract_query_with_additional_sources - |> Repo.transaction() - - create_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) - - case insert_result do - {:ok, %{smart_contract: smart_contract}} -> - {:ok, smart_contract} - - {:error, :smart_contract, changeset, _} -> - {:error, changeset} - - {:error, :set_address_verified, message, _} -> - {:error, message} - end - end - - defp set_address_verified(repo, address_hash) do - query = - from( - address in Address, - where: address.hash == ^address_hash - ) - - case repo.update_all(query, set: [verified: true]) do - {1, _} -> {:ok, []} - _ -> {:error, "There was an error annotating that the address has been verified."} - end - end - - defp set_address_decompiled(repo, address_hash) do - query = - from( - address in Address, - where: address.hash == ^address_hash - ) - - case repo.update_all(query, set: [decompiled: true]) do - {1, _} -> {:ok, []} - _ -> {:error, "There was an error annotating that the address has been decompiled."} - end - end - - defp clear_primary_address_names(repo, address_hash) do - query = - from( - address_name in Address.Name, - where: address_name.address_hash == ^address_hash, - # Enforce Name ShareLocks order (see docs: sharelocks.md) - order_by: [asc: :address_hash, asc: :name], - lock: "FOR NO KEY UPDATE" - ) - - repo.update_all( - from(n in Address.Name, join: s in subquery(query), on: n.address_hash == s.address_hash and n.name == s.name), - set: [primary: false] - ) - - {:ok, []} - end - - defp create_address_name(repo, name, address_hash) do - params = %{ - address_hash: address_hash, - name: name, - primary: true - } - - %Address.Name{} - |> Address.Name.changeset(params) - |> repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name]) - end - - def get_verified_twin_contract(%Explorer.Chain.Address{} = target_address, options \\ []) do - case target_address do - %{contract_code: %Chain.Data{bytes: contract_code_bytes}} -> - target_address_hash = target_address.hash - - contract_code_md5 = Helper.contract_code_md5(contract_code_bytes) - - verified_contract_twin_query = - from( - smart_contract in SmartContract, - where: smart_contract.contract_code_md5 == ^contract_code_md5, - where: smart_contract.address_hash != ^target_address_hash, - select: smart_contract, - limit: 1 - ) - - verified_contract_twin_query - |> select_repo(options).one(timeout: 10_000) - - _ -> - nil - end - end - - @doc """ - Finds metadata for verification of a contract from verified twins: contracts with the same bytecode - which were verified previously, returns a single t:SmartContract.t/0 - """ - def get_address_verified_twin_contract(hash, options \\ []) - - def get_address_verified_twin_contract(hash, options) when is_binary(hash) do - case string_to_address_hash(hash) do - {:ok, address_hash} -> get_address_verified_twin_contract(address_hash, options) - _ -> %{:verified_contract => nil, :additional_sources => nil} - end - end - - def get_address_verified_twin_contract(%Explorer.Chain.Hash{} = address_hash, options) do - with target_address <- select_repo(options).get(Address, address_hash), - false <- is_nil(target_address) do - verified_contract_twin = get_verified_twin_contract(target_address, options) - - verified_contract_twin_additional_sources = get_contract_additional_sources(verified_contract_twin, options) - - %{ - :verified_contract => check_and_update_constructor_args(verified_contract_twin), - :additional_sources => verified_contract_twin_additional_sources - } - else - _ -> - %{:verified_contract => nil, :additional_sources => nil} - end - end - - def get_minimal_proxy_template(address_hash, options \\ []) do - minimal_proxy_template = - case select_repo(options).get(Address, address_hash) do - nil -> - nil - - target_address -> - contract_code = target_address.contract_code - - case contract_code do - %Chain.Data{bytes: contract_code_bytes} -> - contract_bytecode = Base.encode16(contract_code_bytes, case: :lower) - - get_minimal_proxy_from_template_code(contract_bytecode, options) - - _ -> - nil - end - end - - minimal_proxy_template - end - - defp get_minimal_proxy_from_template_code(contract_bytecode, options) do - case contract_bytecode do - "363d3d373d3d3d363d73" <> <> <> _ -> - template_address = "0x" <> template_address - - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^template_address, - select: smart_contract - ) - - template = - query - |> select_repo(options).one(timeout: 10_000) - - template - - _ -> - nil - end - end - - defp get_contract_additional_sources(verified_contract_twin, options) do - if verified_contract_twin do - verified_contract_twin_additional_sources_query = - from( - s in SmartContractAdditionalSource, - where: s.address_hash == ^verified_contract_twin.address_hash - ) - - verified_contract_twin_additional_sources_query - |> select_repo(options).all() - else - [] - end - end - - @spec address_hash_to_smart_contract(Hash.Address.t(), [api?]) :: SmartContract.t() | nil - def address_hash_to_smart_contract(address_hash, options \\ []) do - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^address_hash - ) - - current_smart_contract = select_repo(options).one(query) - - if current_smart_contract do - current_smart_contract - else - address_verified_twin_contract = - get_minimal_proxy_template(address_hash, options) || - get_address_verified_twin_contract(address_hash, options).verified_contract - - if address_verified_twin_contract do - address_verified_twin_contract - |> Map.put(:address_hash, address_hash) - |> Map.put(:metadata_from_verified_twin, true) - |> Map.put(:implementation_address_hash, nil) - |> Map.put(:implementation_name, nil) - |> Map.put(:implementation_fetched_at, nil) - else - current_smart_contract - end - end - end - - @spec address_hash_to_smart_contract(Hash.Address.t()) :: SmartContract.t() | nil - def address_hash_to_one_smart_contract(hash) do - SmartContract - |> where([sc], sc.address_hash == ^hash) - |> Repo.one() - end - - @spec address_hash_to_smart_contract_without_twin(Hash.Address.t(), [api?]) :: SmartContract.t() | nil - def address_hash_to_smart_contract_without_twin(address_hash, options) do - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^address_hash - ) - - select_repo(options).one(query) - end - - def smart_contract_fully_verified?(address_hash, options \\ []) - - def smart_contract_fully_verified?(address_hash_str, options) when is_binary(address_hash_str) do - case string_to_address_hash(address_hash_str) do - {:ok, address_hash} -> - check_fully_verified(address_hash, options) - - _ -> - false - end - end - - def smart_contract_fully_verified?(address_hash, options) do - check_fully_verified(address_hash, options) - end - - defp check_fully_verified(address_hash, options) do - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^address_hash - ) - - result = select_repo(options).one(query) - - if result, do: !result.partially_verified, else: false - end - - def smart_contract_verified?(address_hash_str) when is_binary(address_hash_str) do - case string_to_address_hash(address_hash_str) do - {:ok, address_hash} -> - check_verified(address_hash) - - _ -> - false - end - end - - def smart_contract_verified?(address_hash) do - check_verified(address_hash) - end - - defp check_verified(address_hash) do - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^address_hash - ) - - if Repo.one(query), do: true, else: false - end - - defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil, with_pending? \\ false) do - Transaction - |> order_for_transactions(with_pending?) - |> where_block_number_in_period(from_block, to_block) - |> handle_paging_options(paging_options) - end - - defp order_for_transactions(query, true) do - query - |> order_by([transaction], - desc: transaction.block_number, - desc: transaction.index, - desc: transaction.inserted_at, - asc: transaction.hash - ) - end - - defp order_for_transactions(query, _) do - query - |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) - end - defp fetch_transactions_in_ascending_order_by_index(paging_options) do Transaction |> order_by([transaction], asc: transaction.index) @@ -4024,31 +3177,13 @@ defmodule Explorer.Chain do ) end - defp handle_block_paging_options(query, nil), do: query - - defp handle_block_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query - - defp handle_block_paging_options(query, paging_options) do - query - |> page_block_transactions(paging_options) - |> limit(^paging_options.page_size) - end - - defp handle_paging_options(query, nil), do: query - - defp handle_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query - - defp handle_paging_options(query, paging_options) do - query - |> page_transaction(paging_options) - |> limit(^paging_options.page_size) - end + defp handle_block_paging_options(query, nil), do: query - defp handle_verified_contracts_paging_options(query, nil), do: query + defp handle_block_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query - defp handle_verified_contracts_paging_options(query, paging_options) do + defp handle_block_paging_options(query, paging_options) do query - |> page_verified_contracts(paging_options) + |> page_block_transactions(paging_options) |> limit(^paging_options.page_size) end @@ -4067,7 +3202,7 @@ defmodule Explorer.Chain do query |> (&if(paging_options |> Map.get(:page_number, 1) |> process_page_number() == 1, do: &1, - else: page_transaction(&1, paging_options) + else: Transaction.page_transaction(&1, paging_options) )).() |> handle_page(paging_options) end @@ -4101,8 +3236,8 @@ defmodule Explorer.Chain do def limit_showing_transactions, do: @limit_showing_transactions - defp join_association(query, [{association, nested_preload}], necessity) - when is_atom(association) and is_atom(nested_preload) do + def join_association(query, [{association, nested_preload}], necessity) + when is_atom(association) and is_atom(nested_preload) do case necessity do :optional -> preload(query, [{^association, ^nested_preload}]) @@ -4110,48 +3245,34 @@ defmodule Explorer.Chain do :required -> from(q in query, inner_join: a in assoc(q, ^association), + as: ^association, left_join: b in assoc(a, ^nested_preload), + as: ^nested_preload, preload: [{^association, {a, [{^nested_preload, b}]}}] ) end end - defp join_association(query, association, necessity) when is_atom(association) do - case necessity do - :optional -> - preload(query, ^association) - - :required -> - from(q in query, inner_join: a in assoc(q, ^association), preload: [{^association, a}]) - end - end - - defp join_association(query, association, necessity) do + def join_association(query, association, necessity) do case necessity do :optional -> preload(query, ^association) :required -> - from(q in query, inner_join: a in assoc(q, ^association), preload: [{^association, a}]) + from(q in query, inner_join: a in assoc(q, ^association), as: ^association, preload: [{^association, a}]) end end + @spec join_associations(atom() | Ecto.Query.t(), map) :: Ecto.Query.t() + @doc """ + Function to preload entities associated with selected in provided query items + """ def join_associations(query, necessity_by_association) when is_map(necessity_by_association) do Enum.reduce(necessity_by_association, query, fn {association, join}, acc_query -> join_association(acc_query, association, join) end) end - defp page_addresses(query, %PagingOptions{key: nil}), do: query - - defp page_addresses(query, %PagingOptions{key: {coin_balance, hash}}) do - from(address in query, - where: - (address.fetched_coin_balance == ^coin_balance and address.hash > ^hash) or - address.fetched_coin_balance < ^coin_balance - ) - end - defp page_blocks(query, %PagingOptions{key: nil}), do: query defp page_blocks(query, %PagingOptions{key: {block_number}}) do @@ -4230,46 +3351,6 @@ defmodule Explorer.Chain do where(query, [log], log.index > ^index) end - defp page_pending_transaction(query, %PagingOptions{key: nil}), do: query - - defp page_pending_transaction(query, %PagingOptions{key: {inserted_at, hash}}) do - where( - query, - [transaction], - (is_nil(transaction.block_number) and - (transaction.inserted_at < ^inserted_at or - (transaction.inserted_at == ^inserted_at and transaction.hash > ^hash))) or - not is_nil(transaction.block_number) - ) - end - - defp page_transaction(query, %PagingOptions{key: nil}), do: query - - defp page_transaction(query, %PagingOptions{is_pending_tx: true} = options), - do: page_pending_transaction(query, options) - - defp page_transaction(query, %PagingOptions{key: {block_number, index}, is_index_in_asc_order: true}) do - where( - query, - [transaction], - transaction.block_number < ^block_number or - (transaction.block_number == ^block_number and transaction.index > ^index) - ) - end - - defp page_transaction(query, %PagingOptions{key: {block_number, index}}) do - where( - query, - [transaction], - transaction.block_number < ^block_number or - (transaction.block_number == ^block_number and transaction.index < ^index) - ) - end - - defp page_transaction(query, %PagingOptions{key: {index}}) do - where(query, [transaction], transaction.index < ^index) - end - defp page_block_transactions(query, %PagingOptions{key: nil}), do: query defp page_block_transactions(query, %PagingOptions{key: {_block_number, index}, is_index_in_asc_order: true}) do @@ -4332,12 +3413,6 @@ defmodule Explorer.Chain do ) end - defp page_verified_contracts(query, %PagingOptions{key: nil}), do: query - - defp page_verified_contracts(query, %PagingOptions{key: {id}}) do - where(query, [contract], contract.id < ^id) - end - @doc """ Ensures the following conditions are true: @@ -4367,7 +3442,7 @@ defmodule Explorer.Chain do end @doc """ - The current total number of coins minted minus verifiably burned coins. + The current total number of coins minted minus verifiably burnt coins. """ @spec total_supply :: non_neg_integer() | nil def total_supply do @@ -4414,56 +3489,6 @@ defmodule Explorer.Chain do |> Repo.stream_reduce(initial, reducer) end - @doc """ - Finds all token instances (pairs of contract_address_hash and token_id) which was met in token transfers but has no corresponding entry in token_instances table - """ - @spec stream_not_inserted_token_instances( - initial :: accumulator, - reducer :: (entry :: map(), accumulator -> accumulator) - ) :: {:ok, accumulator} - when accumulator: term() - def stream_not_inserted_token_instances(initial, reducer) when is_function(reducer, 2) do - nft_tokens = - from( - token in Token, - where: token.type == ^"ERC-721" or token.type == ^"ERC-1155", - select: token.contract_address_hash - ) - - token_ids_query = - from( - token_transfer in TokenTransfer, - select: %{ - token_contract_address_hash: token_transfer.token_contract_address_hash, - token_id: fragment("unnest(?)", token_transfer.token_ids) - } - ) - - query = - from( - transfer in subquery(token_ids_query), - inner_join: token in subquery(nft_tokens), - on: token.contract_address_hash == transfer.token_contract_address_hash, - left_join: instance in Instance, - on: - transfer.token_contract_address_hash == instance.token_contract_address_hash and - transfer.token_id == instance.token_id, - where: is_nil(instance.token_id), - select: %{ - contract_address_hash: transfer.token_contract_address_hash, - token_id: transfer.token_id - } - ) - - distinct_query = - from( - q in subquery(query), - distinct: [q.contract_address_hash, q.token_id] - ) - - Repo.stream_reduce(distinct_query, initial, reducer) - end - @doc """ Finds all token instances where metadata never tried to fetch """ @@ -4525,53 +3550,6 @@ defmodule Explorer.Chain do |> Repo.stream_reduce(initial, reducer) end - @doc """ - Returns a list of block numbers token transfer `t:Log.t/0`s that don't have an - associated `t:TokenTransfer.t/0` record. - """ - def uncataloged_token_transfer_block_numbers do - query = - from(l in Log, - as: :log, - where: - l.first_topic == unquote(TokenTransfer.constant()) or - l.first_topic == unquote(TokenTransfer.erc1155_single_transfer_signature()) or - l.first_topic == unquote(TokenTransfer.erc1155_batch_transfer_signature()), - where: - not exists( - from(tf in TokenTransfer, - where: tf.transaction_hash == parent_as(:log).transaction_hash, - where: tf.log_index == parent_as(:log).index - ) - ), - select: l.block_number, - distinct: l.block_number - ) - - Repo.stream_reduce(query, [], &[&1 | &2]) - end - - def decode_contract_address_hash_response(resp) do - case resp do - "0x000000000000000000000000" <> address -> - "0x" <> address - - _ -> - nil - end - end - - def decode_contract_integer_response(resp) do - case resp do - "0x" <> integer_encoded -> - {integer_value, _} = Integer.parse(integer_encoded, 16) - integer_value - - _ -> - nil - end - end - @doc """ Fetches a `t:Token.t/0` by an address hash. @@ -4581,12 +3559,9 @@ defmodule Explorer.Chain do `:required`, and the `t:Token.t/0` has no associated record for that association, then the `t:Token.t/0` will not be included in the list. """ - @spec token_from_address_hash(Hash.Address.t(), [necessity_by_association_option | api?]) :: + @spec token_from_address_hash(Hash.Address.t() | String.t(), [necessity_by_association_option | api?]) :: {:ok, Token.t()} | {:error, :not_found} - def token_from_address_hash( - %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, - options \\ [] - ) do + def token_from_address_hash(hash, options \\ []) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) query = @@ -4648,13 +3623,6 @@ defmodule Explorer.Chain do Repo.exists?(query) end - @spec address_has_rewards?(Address.t()) :: boolean() - def address_has_rewards?(address_hash) do - query = from(r in Reward, where: r.address_hash == ^address_hash) - - Repo.exists?(query) - end - @spec address_tokens_with_balance(Hash.Address.t(), [any()]) :: [] def address_tokens_with_balance(address_hash, paging_options \\ []) do address_hash @@ -4771,14 +3739,6 @@ defmodule Explorer.Chain do ) end - @doc """ - Inserts list of token instances via upsert_token_instance/1. - """ - @spec upsert_token_instances_list([map()]) :: list() - def upsert_token_instances_list(instances) do - Enum.map(instances, &upsert_token_instance/1) - end - @doc """ Update a new `t:Token.t/0` record. @@ -4787,8 +3747,12 @@ defmodule Explorer.Chain do """ @spec update_token(Token.t(), map()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} def update_token(%Token{contract_address_hash: address_hash} = token, params \\ %{}) do - token_changeset = Token.changeset(token, Map.put(params, :updated_at, DateTime.utc_now())) - address_name_changeset = Address.Name.changeset(%Address.Name{}, Map.put(params, :address_hash, address_hash)) + filtered_params = for({key, value} <- params, value !== "" && !is_nil(value), do: {key, value}) |> Enum.into(%{}) + + token_changeset = Token.changeset(token, Map.put(filtered_params, :updated_at, DateTime.utc_now())) + + address_name_changeset = + Address.Name.changeset(%Address.Name{}, Map.put(filtered_params, :address_hash, address_hash)) stale_error_field = :contract_address_hash stale_error_message = "is up to date" @@ -4854,9 +3818,13 @@ defmodule Explorer.Chain do |> select_repo(options).all() end - @spec erc721_or_erc1155_token_instance_from_token_id_and_token_address(non_neg_integer(), Hash.Address.t(), [api?]) :: + @spec nft_instance_from_token_id_and_token_address( + Decimal.t() | non_neg_integer(), + Hash.Address.t(), + [api?] + ) :: {:ok, Instance.t()} | {:error, :not_found} - def erc721_or_erc1155_token_instance_from_token_id_and_token_address(token_id, token_contract_address, options \\ []) do + def nft_instance_from_token_id_and_token_address(token_id, token_contract_address, options \\ []) do query = Instance.token_instance_query(token_id, token_contract_address) case select_repo(options).one(query) do @@ -5136,32 +4104,50 @@ defmodule Explorer.Chain do Repo.one!(query, timeout: :infinity) end - @spec address_to_unique_tokens(Hash.Address.t(), [paging_options | api?]) :: [Instance.t()] - def address_to_unique_tokens(contract_address_hash, options \\ []) do + @spec address_to_unique_tokens(Hash.Address.t(), Token.t(), [paging_options | api?]) :: [Instance.t()] + def address_to_unique_tokens(contract_address_hash, token, options \\ []) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) contract_address_hash |> Instance.address_to_unique_token_instances() |> Instance.page_token_instance(paging_options) |> limit(^paging_options.page_size) + |> preload([_], [:owner]) |> select_repo(options).all() - |> Enum.map(&put_owner_to_token_instance(&1, options)) + |> Enum.map(&put_owner_to_token_instance(&1, token, options)) + end + + @doc """ + Put owner address to unique token instance. If not unique, return original instance. + """ + @spec put_owner_to_token_instance(Instance.t(), Token.t(), [api?]) :: Instance.t() + def put_owner_to_token_instance(token_instance, token, options \\ []) + + def put_owner_to_token_instance(%Instance{is_unique: nil} = token_instance, token, options) do + put_owner_to_token_instance(Instance.put_is_unique(token_instance, token, options), token, options) end - def put_owner_to_token_instance(%Instance{} = token_instance, options \\ []) do - owner = + def put_owner_to_token_instance( + %Instance{owner: nil, is_unique: true} = token_instance, + %Token{type: type}, + options + ) + when type in ["ERC-1155", "ERC-404"] do + owner_address_hash = token_instance |> Instance.owner_query() |> select_repo(options).one() - %{token_instance | owner: owner} + %{token_instance | owner: select_repo(options).get_by(Address, hash: owner_address_hash)} end + def put_owner_to_token_instance(%Instance{} = token_instance, _token, _options), do: token_instance + @spec data() :: Dataloader.Ecto.t() def data, do: DataloaderEcto.new(Repo) @spec transaction_token_transfer_type(Transaction.t()) :: - :erc20 | :erc721 | :erc1155 | :token_transfer | nil + :erc20 | :erc721 | :erc1155 | :erc404 | :token_transfer | nil def transaction_token_transfer_type( %Transaction{ status: :ok, @@ -5222,10 +4208,7 @@ defmodule Explorer.Chain do find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address}) - {"0xf907fc5b" <> _params, ^zero_wei} -> - :erc20 - - # check for ERC-20 or for old ERC-721, ERC-1155 token versions + # check for ERC-20 or for old ERC-721, ERC-1155, ERC-404 token versions {unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} -> types = [:address, {:uint, 256}] @@ -5233,7 +4216,7 @@ defmodule Explorer.Chain do decimal_value = Decimal.new(value) - find_erc721_or_erc20_or_erc1155_token_transfer(transaction.token_transfers, {address, decimal_value}) + find_known_token_transfer(transaction.token_transfers, {address, decimal_value}) _ -> nil @@ -5258,7 +4241,7 @@ defmodule Explorer.Chain do if token_transfer, do: :erc1155 end - defp find_erc721_or_erc20_or_erc1155_token_transfer(token_transfers, {address, decimal_value}) do + defp find_known_token_transfer(token_transfers, {address, decimal_value}) do token_transfer = Enum.find(token_transfers, fn token_transfer -> token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value @@ -5269,6 +4252,7 @@ defmodule Explorer.Chain do %Token{type: "ERC-20"} -> :erc20 %Token{type: "ERC-721"} -> :erc721 %Token{type: "ERC-1155"} -> :erc1155 + %Token{type: "ERC-404"} -> :erc404 _ -> nil end else @@ -5335,72 +4319,18 @@ defmodule Explorer.Chain do Repo.one(query) end - @spec is_erc_20_token?(Token.t()) :: bool - def is_erc_20_token?(token) do - is_erc_20_token_type?(token.type) + @spec erc_20_token?(Token.t()) :: bool + def erc_20_token?(token) do + erc_20_token_type?(token.type) end - defp is_erc_20_token_type?(type) do + defp erc_20_token_type?(type) do case type do "ERC-20" -> true _ -> false end end - @doc """ - Checks if an `t:Explorer.Chain.Address.t/0` with the given `hash` exists. - - Returns `:ok` if found - - iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address( - ...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"} - ...> ) - iex> Explorer.Chain.check_address_exists(hash) - :ok - - Returns `:not_found` if not found - - iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - iex> Explorer.Chain.check_address_exists(hash) - :not_found - - """ - @spec check_address_exists(Hash.Address.t()) :: :ok | :not_found - def check_address_exists(address_hash) do - address_hash - |> address_exists?() - |> boolean_to_check_result() - end - - @doc """ - Checks if an `t:Explorer.Chain.Address.t/0` with the given `hash` exists. - - Returns `true` if found - - iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address( - ...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"} - ...> ) - iex> Explorer.Chain.address_exists?(hash) - true - - Returns `false` if not found - - iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - iex> Explorer.Chain.address_exists?(hash) - false - - """ - @spec address_exists?(Hash.Address.t()) :: boolean() - def address_exists?(address_hash) do - query = - from( - address in Address, - where: address.hash == ^address_hash - ) - - Repo.exists?(query) - end - @doc """ Checks if it exists an `t:Explorer.Chain.Address.t/0` that has the provided `t:Explorer.Chain.Address.t/0` `hash` and a contract. @@ -5460,36 +4390,6 @@ defmodule Explorer.Chain do Repo.exists?(query) end - @doc """ - Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the - `t:Explorer.Chain.Address.t/0` with the provided `hash`. - - Returns `:ok` if found and `:not_found` otherwise. - """ - @spec check_verified_smart_contract_exists(Hash.Address.t()) :: :ok | :not_found - def check_verified_smart_contract_exists(address_hash) do - address_hash - |> verified_smart_contract_exists?() - |> boolean_to_check_result() - end - - @doc """ - Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the - `t:Explorer.Chain.Address.t/0` with the provided `hash`. - - Returns `true` if found and `false` otherwise. - """ - @spec verified_smart_contract_exists?(Hash.Address.t()) :: boolean() - def verified_smart_contract_exists?(address_hash) do - query = - from( - smart_contract in SmartContract, - where: smart_contract.address_hash == ^address_hash - ) - - Repo.exists?(query) - end - @doc """ Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists. @@ -5603,20 +4503,20 @@ defmodule Explorer.Chain do ...> token_contract_address_hash: token.contract_address_hash, ...> token_id: token_id ...> ) - iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(token_id, token.contract_address_hash) + iex> Explorer.Chain.check_nft_instance_exists(token_id, token.contract_address_hash) :ok Returns `:not_found` if not found iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - iex> Explorer.Chain.check_erc721_or_erc1155_token_instance_exists(10, hash) + iex> Explorer.Chain.check_nft_instance_exists(10, hash) :not_found """ - @spec check_erc721_or_erc1155_token_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) :: + @spec check_nft_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) :: :ok | :not_found - def check_erc721_or_erc1155_token_instance_exists(token_id, hash) do + def check_nft_instance_exists(token_id, hash) do token_id - |> erc721_or_erc1155_token_instance_exist?(hash) + |> nft_instance_exist?(hash) |> boolean_to_check_result() end @@ -5631,17 +4531,17 @@ defmodule Explorer.Chain do ...> token_contract_address_hash: token.contract_address_hash, ...> token_id: token_id ...> ) - iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(token_id, token.contract_address_hash) + iex> Explorer.Chain.nft_instance_exist?(token_id, token.contract_address_hash) true Returns `false` if not found iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - iex> Explorer.Chain.erc721_or_erc1155_token_instance_exist?(10, hash) + iex> Explorer.Chain.nft_instance_exist?(10, hash) false """ - @spec erc721_or_erc1155_token_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean() - def erc721_or_erc1155_token_instance_exist?(token_id, hash) do + @spec nft_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean() + def nft_instance_exist?(token_id, hash) do query = from(i in Instance, where: i.token_contract_address_hash == ^hash and i.token_id == ^Decimal.new(token_id) @@ -5650,9 +4550,9 @@ defmodule Explorer.Chain do Repo.exists?(query) end - defp boolean_to_check_result(true), do: :ok + def boolean_to_check_result(true), do: :ok - defp boolean_to_check_result(false), do: :not_found + def boolean_to_check_result(false), do: :not_found @doc """ Fetches the first trace from the Nethermind trace URL. @@ -5670,88 +4570,6 @@ defmodule Explorer.Chain do end end - def combine_proxy_implementation_abi(smart_contract, options \\ []) - - def combine_proxy_implementation_abi(%SmartContract{abi: abi} = smart_contract, options) when not is_nil(abi) do - implementation_abi = get_implementation_abi_from_proxy(smart_contract, options) - - if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi - end - - def combine_proxy_implementation_abi(_, _) do - [] - end - - def gnosis_safe_contract?(abi) when not is_nil(abi) do - implementation_method_abi = - abi - |> Enum.find(fn method -> - master_copy_pattern?(method) - end) - - if implementation_method_abi, do: true, else: false - end - - def gnosis_safe_contract?(abi) when is_nil(abi), do: false - - def master_copy_pattern?(method) do - Map.get(method, "type") == "constructor" && - method - |> Enum.find(fn item -> - case item do - {"inputs", inputs} -> - master_copy_input?(inputs) - - _ -> - false - end - end) - end - - defp master_copy_input?(inputs) do - inputs - |> Enum.find(fn input -> - Map.get(input, "name") == "_masterCopy" - end) - end - - def get_implementation_abi(implementation_address_hash_string, options \\ []) - - def get_implementation_abi(implementation_address_hash_string, options) - when not is_nil(implementation_address_hash_string) do - case Chain.string_to_address_hash(implementation_address_hash_string) do - {:ok, implementation_address_hash} -> - implementation_smart_contract = - implementation_address_hash - |> address_hash_to_smart_contract(options) - - if implementation_smart_contract do - implementation_smart_contract - |> Map.get(:abi) - else - [] - end - - _ -> - [] - end - end - - def get_implementation_abi(implementation_address_hash_string, _) when is_nil(implementation_address_hash_string) do - [] - end - - def get_implementation_abi_from_proxy( - %SmartContract{address_hash: proxy_address_hash, abi: abi} = smart_contract, - options - ) - when not is_nil(proxy_address_hash) and not is_nil(abi) do - {implementation_address_hash_string, _name} = SmartContract.get_implementation_address_hash(smart_contract, options) - get_implementation_abi(implementation_address_hash_string) - end - - def get_implementation_abi_from_proxy(_, _), do: [] - defp format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) do {:ok, to_address_hash} = if Map.has_key?(first_trace, :to_address_hash) do @@ -5848,7 +4666,7 @@ defmodule Explorer.Chain do if transaction_index == 0 do 0 else - filtered_block_numbers = EthereumJSONRPC.are_block_numbers_in_range?([block_number]) + filtered_block_numbers = RangesHelper.filter_traceable_block_numbers([block_number]) {:ok, traces} = fetch_block_internal_transactions(filtered_block_numbers, json_rpc_named_arguments) sorted_traces = @@ -5878,7 +4696,7 @@ defmodule Explorer.Chain do @spec get_token_transfer_type(TokenTransfer.t()) :: :token_burning | :token_minting | :token_spawning | :token_transfer def get_token_transfer_type(transfer) do - {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) + {:ok, burn_address_hash} = Chain.string_to_address_hash(SmartContract.burn_address_hash_string()) cond do transfer.to_address_hash == burn_address_hash && transfer.from_address_hash !== burn_address_hash -> @@ -5922,10 +4740,12 @@ defmodule Explorer.Chain do end end - defp from_block(options) do + @spec from_block(keyword) :: any + def from_block(options) do Keyword.get(options, :from_block) || nil end + @spec to_block(keyword) :: any def to_block(options) do Keyword.get(options, :to_block) || nil end @@ -5956,9 +4776,9 @@ defmodule Explorer.Chain do |> Repo.one() end - def is_address_hash_is_smart_contract?(nil), do: false + def address_hash_is_smart_contract?(nil), do: false - def is_address_hash_is_smart_contract?(address_hash) do + def address_hash_is_smart_contract?(address_hash) do with %Address{contract_code: bytecode} <- Repo.get_by(Address, hash: address_hash), false <- is_nil(bytecode) do true @@ -6066,6 +4886,11 @@ defmodule Explorer.Chain do as: :created_token ) ) + + :blob_transaction -> + dynamic + |> filter_blob_transaction_dynamic() + |> apply_filter_by_tx_type_to_transactions_inner(remain, query) end end @@ -6099,54 +4924,11 @@ defmodule Explorer.Chain do dynamic([tx, created_token: created_token], ^dynamic or not is_nil(created_token)) end - @spec verified_contracts([ - paging_options - | necessity_by_association_option - | {:filter, :solidity | :vyper} - | {:search, String.t() | {:api?, true | false}} - ]) :: [SmartContract.t()] - def verified_contracts(options \\ []) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - filter = Keyword.get(options, :filter, nil) - search_string = Keyword.get(options, :search, nil) - - query = from(contract in SmartContract, select: contract, order_by: [desc: :id]) - - query - |> filter_contracts(filter) - |> search_contracts(search_string) - |> handle_verified_contracts_paging_options(paging_options) - |> join_associations(necessity_by_association) - |> select_repo(options).all() - end - - defp search_contracts(basic_query, nil), do: basic_query - - defp search_contracts(basic_query, search_string) do - from(contract in basic_query, - where: - ilike(contract.name, ^"%#{search_string}%") or - ilike(fragment("'0x' || encode(?, 'hex')", contract.address_hash), ^"%#{search_string}%") - ) - end - - defp filter_contracts(basic_query, :solidity) do - basic_query - |> where(is_vyper_contract: ^false) - end - - defp filter_contracts(basic_query, :vyper) do - basic_query - |> where(is_vyper_contract: ^true) - end - - defp filter_contracts(basic_query, :yul) do - from(query in basic_query, where: is_nil(query.abi)) + def filter_blob_transaction_dynamic(dynamic) do + # EIP-2718 blob transaction type + dynamic([tx], ^dynamic or tx.type == 3) end - defp filter_contracts(basic_query, _), do: basic_query - def count_verified_contracts do Repo.aggregate(SmartContract, :count, timeout: :infinity) end @@ -6397,34 +5179,6 @@ defmodule Explorer.Chain do limit(query, ^coin_balances_fetcher_limit) end - def put_has_token_transfers_to_tx(query, true), do: query - - def put_has_token_transfers_to_tx(query, false) do - from(tx in query, - select_merge: %{ - has_token_transfers: - fragment( - "(SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NOT NULL", - tx.hash - ) - } - ) - end - - @spec verified_contracts_top(non_neg_integer()) :: [Hash.Address.t()] - def verified_contracts_top(limit) do - query = - from(contract in SmartContract, - inner_join: address in Address, - on: contract.address_hash == address.hash, - order_by: [desc: address.transactions_count], - limit: ^limit, - select: contract.address_hash - ) - - Repo.all(query) - end - @spec default_paging_options() :: map() def default_paging_options do @default_paging_options diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 0b38b0801448..a240fb8552d8 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -7,7 +7,9 @@ defmodule Explorer.Chain.Address do use Explorer.Schema + alias Ecto.Association.NotLoaded alias Ecto.Changeset + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{ Address, @@ -17,14 +19,13 @@ defmodule Explorer.Chain.Address do Hash, InternalTransaction, SmartContract, - SmartContractAdditionalSource, Token, Transaction, Wei, Withdrawal } - alias Explorer.Chain.Cache.NetVersion + alias Explorer.Chain.Cache.{Accounts, NetVersion} @optional_attrs ~w(contract_code fetched_coin_balance fetched_coin_balance_block_number nonce decompiled verified gas_used transactions_count token_transfers_count)a @required_attrs ~w(hash)a @@ -35,36 +36,6 @@ defmodule Explorer.Chain.Address do """ @type hash :: Hash.t() - @typedoc """ - * `fetched_coin_balance` - The last fetched balance from Nethermind - * `fetched_coin_balance_block_number` - the `t:Explorer.Chain.Block.t/0` `t:Explorer.Chain.Block.block_number/0` for - which `fetched_coin_balance` was fetched - * `hash` - the hash of the address's public key - * `contract_code` - the binary code of the contract when an Address is a contract. The human-readable - Solidity source code is in `smart_contract` `t:Explorer.Chain.SmartContract.t/0` `contract_source_code` *if* the - contract has been verified - * `names` - names known for the address - * `inserted_at` - when this address was inserted - * `updated_at` when this address was last updated - - `fetched_coin_balance` and `fetched_coin_balance_block_number` may be updated when a new coin_balance row is fetched. - They may also be updated when the balance is fetched via the on demand fetcher. - """ - @type t :: %__MODULE__{ - fetched_coin_balance: Wei.t(), - fetched_coin_balance_block_number: Block.block_number(), - hash: Hash.Address.t(), - contract_code: Data.t() | nil, - names: %Ecto.Association.NotLoaded{} | [Address.Name.t()], - contracts_creation_transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), - inserted_at: DateTime.t(), - updated_at: DateTime.t(), - nonce: non_neg_integer() | nil, - transactions_count: non_neg_integer() | nil, - token_transfers_count: non_neg_integer() | nil, - gas_used: non_neg_integer() | nil - } - @derive {Poison.Encoder, except: [ :__meta__, @@ -73,8 +44,7 @@ defmodule Explorer.Chain.Address do :token, :contracts_creation_internal_transaction, :contracts_creation_transaction, - :names, - :smart_contract_additional_sources + :names ]} @derive {Jason.Encoder, @@ -85,14 +55,30 @@ defmodule Explorer.Chain.Address do :token, :contracts_creation_internal_transaction, :contracts_creation_transaction, - :names, - :smart_contract_additional_sources + :names ]} - @primary_key {:hash, Hash.Address, autogenerate: false} - schema "addresses" do + @typedoc """ + * `fetched_coin_balance` - The last fetched balance from Nethermind + * `fetched_coin_balance_block_number` - the `t:Explorer.Chain.Block.t/0` `t:Explorer.Chain.Block.block_number/0` for + which `fetched_coin_balance` was fetched + * `hash` - the hash of the address's public key + * `contract_code` - the binary code of the contract when an Address is a contract. The human-readable + Solidity source code is in `smart_contract` `t:Explorer.Chain.SmartContract.t/0` `contract_source_code` *if* the + contract has been verified + * `names` - names known for the address + * `inserted_at` - when this address was inserted + * `updated_at` - when this address was last updated + * `ens_domain_name` - virtual field for ENS domain name passing + + `fetched_coin_balance` and `fetched_coin_balance_block_number` may be updated when a new coin_balance row is fetched. + They may also be updated when the balance is fetched via the on demand fetcher. + """ + @primary_key false + typed_schema "addresses" do + field(:hash, Hash.Address, primary_key: true) field(:fetched_coin_balance, Wei) - field(:fetched_coin_balance_block_number, :integer) + field(:fetched_coin_balance_block_number, :integer) :: Block.block_number() | nil field(:contract_code, Data) field(:nonce, :integer) field(:decompiled, :boolean, default: false) @@ -102,26 +88,28 @@ defmodule Explorer.Chain.Address do field(:transactions_count, :integer) field(:token_transfers_count, :integer) field(:gas_used, :integer) + field(:ens_domain_name, :string, virtual: true) - has_one(:smart_contract, SmartContract) - has_one(:token, Token, foreign_key: :contract_address_hash) + has_one(:smart_contract, SmartContract, references: :hash) + has_one(:token, Token, foreign_key: :contract_address_hash, references: :hash) has_one( :contracts_creation_internal_transaction, InternalTransaction, - foreign_key: :created_contract_address_hash + foreign_key: :created_contract_address_hash, + references: :hash ) has_one( :contracts_creation_transaction, Transaction, - foreign_key: :created_contract_address_hash + foreign_key: :created_contract_address_hash, + references: :hash ) - has_many(:names, Address.Name, foreign_key: :address_hash) - has_many(:decompiled_smart_contracts, DecompiledSmartContract, foreign_key: :address_hash) - has_many(:smart_contract_additional_sources, SmartContractAdditionalSource, foreign_key: :address_hash) - has_many(:withdrawals, Withdrawal, foreign_key: :address_hash) + has_many(:names, Address.Name, foreign_key: :address_hash, references: :hash) + has_many(:decompiled_smart_contracts, DecompiledSmartContract, foreign_key: :address_hash, references: :hash) + has_many(:withdrawals, Withdrawal, foreign_key: :address_hash, references: :hash) timestamps() end @@ -156,6 +144,11 @@ defmodule Explorer.Chain.Address do checksum(hash, iodata?) end + def checksum(hash_string, iodata?) when is_binary(hash_string) do + {:ok, hash} = Chain.string_to_address_hash(hash_string) + checksum(hash, iodata?) + end + def checksum(hash, iodata?) do checksum_formatted = case Application.get_env(:explorer, :checksum_function) || :eth do @@ -252,6 +245,16 @@ defmodule Explorer.Chain.Address do end) end + @doc """ + Preloads provided contracts associations if address has contract_code which is not nil + """ + @spec maybe_preload_smart_contract_associations(Address.t(), list, list) :: Address.t() + def maybe_preload_smart_contract_associations(%Address{contract_code: nil} = address, _associations, _options), + do: address + + def maybe_preload_smart_contract_associations(%Address{contract_code: _} = address, associations, options), + do: Chain.select_repo(options).preload(address, associations) + @doc """ Counts all the addresses where the `fetched_coin_balance` is > 0. """ @@ -293,4 +296,131 @@ defmodule Explorer.Chain.Address do @for.checksum(address) end end + + @default_paging_options %PagingOptions{page_size: 50} + @doc """ + Lists the top `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance and address hash. + + """ + @spec list_top_addresses :: [{Address.t(), non_neg_integer()}] + def list_top_addresses(options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + if is_nil(paging_options.key) do + paging_options.page_size + |> Accounts.take_enough() + |> case do + nil -> + get_addresses(options) + + accounts -> + Enum.map( + accounts, + &{&1, &1.transactions_count || 0} + ) + end + else + fetch_top_addresses(options) + end + end + + @doc """ + Checks if given address is smart-contract + """ + @spec smart_contract?(any()) :: boolean() | nil + def smart_contract?(%__MODULE__{contract_code: nil}), do: false + def smart_contract?(%__MODULE__{contract_code: _}), do: true + def smart_contract?(%NotLoaded{}), do: nil + def smart_contract?(_), do: false + + defp get_addresses(options) do + accounts_with_n = fetch_top_addresses(options) + + accounts_with_n + |> Enum.map(fn {address, _n} -> address end) + |> Accounts.update() + + accounts_with_n + end + + defp fetch_top_addresses(options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + base_query = + from(a in Address, + where: a.fetched_coin_balance > ^0, + order_by: [desc: a.fetched_coin_balance, asc: a.hash], + preload: [:names, :smart_contract], + select: {a, a.transactions_count} + ) + + base_query + |> page_addresses(paging_options) + |> limit(^paging_options.page_size) + |> Chain.select_repo(options).all() + end + + defp page_addresses(query, %PagingOptions{key: nil}), do: query + + defp page_addresses(query, %PagingOptions{key: {coin_balance, hash}}) do + from(address in query, + where: + (address.fetched_coin_balance == ^coin_balance and address.hash > ^hash) or + address.fetched_coin_balance < ^coin_balance + ) + end + + @doc """ + Checks if an `t:Explorer.Chain.Address.t/0` with the given `hash` exists. + + Returns `:ok` if found + + iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address( + ...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"} + ...> ) + iex> Explorer.Address.check_address_exists(hash) + :ok + + Returns `:not_found` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + iex> Explorer.Address.check_address_exists(hash) + :not_found + + """ + @spec check_address_exists(Hash.Address.t(), [Chain.api?()]) :: :ok | :not_found + def check_address_exists(address_hash, options \\ []) do + address_hash + |> address_exists?(options) + |> Chain.boolean_to_check_result() + end + + @doc """ + Checks if an `t:Explorer.Chain.Address.t/0` with the given `hash` exists. + + Returns `true` if found + + iex> {:ok, %Explorer.Chain.Address{hash: hash}} = Explorer.Chain.create_address( + ...> %{hash: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed"} + ...> ) + iex> Explorer.Chain.Address.address_exists?(hash) + true + + Returns `false` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + iex> Explorer.Chain.Address.address_exists?(hash) + false + + """ + @spec address_exists?(Hash.Address.t(), [Chain.api?()]) :: boolean() + def address_exists?(address_hash, options \\ []) do + query = + from( + address in Address, + where: address.hash == ^address_hash + ) + + Chain.select_repo(options).exists?(query) + end end diff --git a/apps/explorer/lib/explorer/chain/address/coin_balance.ex b/apps/explorer/lib/explorer/chain/address/coin_balance.ex index b2e5a46f263e..de246c1ba4a3 100644 --- a/apps/explorer/lib/explorer/chain/address/coin_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/coin_balance.ex @@ -27,18 +27,9 @@ defmodule Explorer.Chain.Address.CoinBalance do given `address`, the `t:Explorer.Chain.Address.t/0` `fetched_coin_balance` will match this value. * `value_fetched_at` - when `value` was fetched. """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t(), - block_number: Block.block_number(), - inserted_at: DateTime.t(), - updated_at: DateTime.t(), - value: Wei.t() | nil - } - @primary_key false - schema "address_coin_balances" do - field(:block_number, :integer) + typed_schema "address_coin_balances" do + field(:block_number, :integer) :: Block.block_number() field(:value, Wei) field(:value_fetched_at, :utc_datetime_usec) field(:delta, Wei, virtual: true) @@ -47,7 +38,7 @@ defmodule Explorer.Chain.Address.CoinBalance do timestamps() - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/address/coin_balance_daily.ex b/apps/explorer/lib/explorer/chain/address/coin_balance_daily.ex index cc881d9c471d..fdd7b9e9fc60 100644 --- a/apps/explorer/lib/explorer/chain/address/coin_balance_daily.ex +++ b/apps/explorer/lib/explorer/chain/address/coin_balance_daily.ex @@ -21,23 +21,14 @@ defmodule Explorer.Chain.Address.CoinBalanceDaily do * `updated_at` - When the balance was last updated. * `value` - the max balance (`value`) of `address` during the `day`. """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t(), - day: Date.t(), - inserted_at: DateTime.t(), - updated_at: DateTime.t(), - value: Wei.t() | nil - } - @primary_key false - schema "address_coin_balances_daily" do - field(:day, :date) + typed_schema "address_coin_balances_daily" do + field(:day, :date, null: false) field(:value, Wei) timestamps() - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index 9f0aea47d855..bdd08ecb72c9 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -92,7 +92,7 @@ defmodule Explorer.Chain.Address.Counters do if is_nil(cached_value) || cached_value == 0 do count = CacheHelper.estimated_count_from("addresses", options) - max(count, 0) + if is_nil(count), do: 0, else: max(count, 0) else cached_value end @@ -483,7 +483,7 @@ defmodule Explorer.Chain.Address.Counters do case res do {:ok, {txs_type, txs_hashes}} when txs_type in @txs_types -> acc - |> (&Map.put(&1, :txs_types, [txs_type | &1[:txs_types] || []])).() + |> (&Map.put(&1, :txs_types, [txs_type | &1[:txs_types]])).() |> (&Map.put(&1, :txs_hashes, &1[:txs_hashes] ++ txs_hashes)).() {:ok, {type, counter}} -> diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 50af3af4dd5d..43e1a57099b8 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -27,40 +27,30 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do * `token_id` - The token_id of the transferred token (applicable for ERC-1155) * `token_type` - The type of the token """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t(), - token: %Ecto.Association.NotLoaded{} | Token.t(), - token_contract_address_hash: Hash.Address, - block_number: Block.block_number(), - max_block_number: Block.block_number(), - inserted_at: DateTime.t(), - updated_at: DateTime.t(), - value: Decimal.t() | nil, - token_id: non_neg_integer() | nil, - token_type: String.t() - } - - schema "address_current_token_balances" do + typed_schema "address_current_token_balances" do field(:value, :decimal) - field(:block_number, :integer) - field(:max_block_number, :integer, virtual: true) + field(:block_number, :integer) :: Block.block_number() + field(:max_block_number, :integer, virtual: true) :: Block.block_number() field(:value_fetched_at, :utc_datetime_usec) field(:token_id, :decimal) - field(:token_type, :string) + field(:token_type, :string, null: false) field(:fiat_value, :decimal, virtual: true) + field(:distinct_token_instances_count, :integer, virtual: true) + field(:token_ids, {:array, :decimal}, virtual: true) + field(:preloaded_token_instances, {:array, :any}, virtual: true) # A transient field for deriving token holder count deltas during address_current_token_balances upserts field(:old_value, :decimal) - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) belongs_to( :token, Token, foreign_key: :token_contract_address_hash, references: :contract_address_hash, - type: Hash.Address + type: Hash.Address, + null: false ) timestamps() @@ -170,9 +160,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do on: ctb.token_contract_address_hash == t.contract_address_hash, preload: [token: t], select: ctb, - select_merge: ^%{fiat_value: fiat_balance}, - order_by: ^[desc_nulls_last: fiat_balance], - order_by: [desc: ctb.value, desc: ctb.id] + select_merge: ^%{fiat_value: fiat_balance} ) end diff --git a/apps/explorer/lib/explorer/chain/address/name.ex b/apps/explorer/lib/explorer/chain/address/name.ex index 4364adf98795..d05e04ae509c 100644 --- a/apps/explorer/lib/explorer/chain/address/name.ex +++ b/apps/explorer/lib/explorer/chain/address/name.ex @@ -7,29 +7,24 @@ defmodule Explorer.Chain.Address.Name do import Ecto.Changeset - alias Ecto.Changeset + alias Ecto.{Changeset, Repo} alias Explorer.Chain.{Address, Hash} + import Ecto.Query, only: [from: 2] + @typedoc """ * `address` - the `t:Explorer.Chain.Address.t/0` with `value` at end of `block_number`. * `address_hash` - foreign key for `address`. * `name` - name for the address * `primary` - flag for if the name is the primary name for the address """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t(), - name: String.t(), - primary: boolean(), - metadata: map() - } - - @primary_key {:id, :integer, autogenerate: false} - schema "address_names" do - field(:name, :string) + @primary_key false + typed_schema "address_names" do + field(:id, :integer, autogenerate: false, primary_key: true, null: false) + field(:name, :string, null: false) field(:primary, :boolean) field(:metadata, :map) - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) timestamps() end @@ -46,6 +41,46 @@ defmodule Explorer.Chain.Address.Name do |> foreign_key_constraint(:address_hash) end + @doc """ + Sets primary false for all primary names for the given address hash + """ + @spec clear_primary_address_names(Repo.t(), Hash.Address.t()) :: {:ok, []} + def clear_primary_address_names(repo, address_hash) do + query = + from( + address_name in __MODULE__, + where: address_name.address_hash == ^address_hash, + where: address_name.primary == true, + # Enforce Name ShareLocks order (see docs: sharelocks.md) + order_by: [asc: :address_hash, asc: :name], + lock: "FOR NO KEY UPDATE" + ) + + repo.update_all( + from(n in __MODULE__, join: s in subquery(query), on: n.address_hash == s.address_hash and n.name == s.name), + set: [primary: false] + ) + + {:ok, []} + end + + @doc """ + Creates primary address name for the given address hash + """ + @spec create_primary_address_name(Repo.t(), String.t(), Hash.Address.t()) :: + {:ok, [__MODULE__.t()]} | {:error, [Changeset.t()]} + def create_primary_address_name(repo, name, address_hash) do + params = %{ + address_hash: address_hash, + name: name, + primary: true + } + + %__MODULE__{} + |> changeset(params) + |> repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name]) + end + defp trim_name(%Changeset{valid?: false} = changeset), do: changeset defp trim_name(%Changeset{valid?: true} = changeset) do diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index ab7a75b62486..e50462b1e96c 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -13,6 +13,7 @@ defmodule Explorer.Chain.Address.TokenBalance do alias Explorer.Chain alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.Chain.{Address, Block, Hash, Token} @typedoc """ @@ -22,37 +23,25 @@ defmodule Explorer.Chain.Address.TokenBalance do * `token_contract_address_hash` - The contract address hash foreign key. * `block_number` - The block's number that the transfer took place. * `value` - The value that's represents the balance. - * `token_id` - The token_id of the transferred token (applicable for ERC-1155 and ERC-721 tokens) + * `token_id` - The token_id of the transferred token (applicable for ERC-1155, ERC-721 and ERC-404 tokens) * `token_type` - The type of the token """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t(), - token: %Ecto.Association.NotLoaded{} | Token.t(), - token_contract_address_hash: Hash.Address, - block_number: Block.block_number(), - inserted_at: DateTime.t(), - updated_at: DateTime.t(), - value: Decimal.t() | nil, - token_id: non_neg_integer() | nil, - token_type: String.t() - } - - schema "address_token_balances" do + typed_schema "address_token_balances" do field(:value, :decimal) - field(:block_number, :integer) + field(:block_number, :integer) :: Block.block_number() field(:value_fetched_at, :utc_datetime_usec) field(:token_id, :decimal) - field(:token_type, :string) + field(:token_type, :string, null: false) - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) belongs_to( :token, Token, foreign_key: :token_contract_address_hash, references: :contract_address_hash, - type: Hash.Address + type: Hash.Address, + null: false ) timestamps() @@ -80,15 +69,27 @@ defmodule Explorer.Chain.Address.TokenBalance do ignores the burn_address for tokens ERC-721 since the most tokens ERC-721 don't allow get the balance for burn_address. """ + # credo:disable-for-next-line /Complexity/ def unfetched_token_balances do - from( - tb in TokenBalance, - join: t in Token, - on: tb.token_contract_address_hash == t.contract_address_hash, - where: - ((tb.address_hash != ^@burn_address_hash and t.type == "ERC-721") or t.type == "ERC-20" or t.type == "ERC-1155") and - (is_nil(tb.value_fetched_at) or is_nil(tb.value)) - ) + if BackgroundMigrations.get_tb_token_type_finished() do + from( + tb in TokenBalance, + where: + ((tb.address_hash != ^@burn_address_hash and tb.token_type == "ERC-721") or tb.token_type == "ERC-20" or + tb.token_type == "ERC-1155" or tb.token_type == "ERC-404") and + (is_nil(tb.value_fetched_at) or is_nil(tb.value)) + ) + else + from( + tb in TokenBalance, + join: t in Token, + on: tb.token_contract_address_hash == t.contract_address_hash, + where: + ((tb.address_hash != ^@burn_address_hash and t.type == "ERC-721") or t.type == "ERC-20" or + t.type == "ERC-1155" or t.type == "ERC-404") and + (is_nil(tb.value_fetched_at) or is_nil(tb.value)) + ) + end end @doc """ diff --git a/apps/explorer/lib/explorer/chain/beacon/blob.ex b/apps/explorer/lib/explorer/chain/beacon/blob.ex new file mode 100644 index 000000000000..d6cb28a27402 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/beacon/blob.ex @@ -0,0 +1,47 @@ +defmodule Explorer.Chain.Beacon.Blob do + @moduledoc "Models a data blob broadcasted using eip4844 blob transactions." + + use Explorer.Schema + + alias Explorer.Chain.{Data, Hash} + + @required_attrs ~w(hash blob_data kzg_commitment kzg_proof)a + + @type t :: %__MODULE__{ + hash: Hash.t(), + blob_data: Data.t(), + kzg_commitment: Data.t(), + kzg_proof: Data.t() + } + + @primary_key {:hash, Hash.Full, autogenerate: false} + schema "beacon_blobs" do + field(:blob_data, Data) + field(:kzg_commitment, Data) + field(:kzg_proof, Data) + + timestamps(updated_at: false) + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = struct, attrs \\ %{}) do + struct + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:hash) + end + + @doc """ + Returns the `hash` of the `t:Explorer.Chain.Beacon.Blob.t/0` as per EIP-4844. + """ + @spec hash(binary()) :: Hash.Full.t() + def hash(kzg_commitment) do + raw_hash = :crypto.hash(:sha256, kzg_commitment) + <<_::size(8), rest::binary>> = raw_hash + {:ok, hash} = Hash.Full.cast(<<1>> <> rest) + hash + end +end diff --git a/apps/explorer/lib/explorer/chain/beacon/blob_transaction.ex b/apps/explorer/lib/explorer/chain/beacon/blob_transaction.ex new file mode 100644 index 000000000000..59e3d400fefb --- /dev/null +++ b/apps/explorer/lib/explorer/chain/beacon/blob_transaction.ex @@ -0,0 +1,47 @@ +defmodule Explorer.Chain.Beacon.BlobTransaction do + @moduledoc "Models a transaction extension with extra fields from eip4844 blob transactions." + + use Explorer.Schema + + alias Explorer.Chain.{Hash, Transaction} + + @required_attrs ~w(hash max_fee_per_blob_gas blob_gas_price blob_gas_used blob_versioned_hashes)a + + @type t :: %__MODULE__{ + hash: Hash.t(), + max_fee_per_blob_gas: Decimal.t(), + blob_gas_price: Decimal.t(), + blob_gas_used: Decimal.t(), + blob_versioned_hashes: [Hash.t()] + } + + @primary_key {:hash, Hash.Full, autogenerate: false} + schema "beacon_blobs_transactions" do + field(:max_fee_per_blob_gas, :decimal) + field(:blob_gas_price, :decimal) + field(:blob_gas_used, :decimal) + field(:blob_versioned_hashes, {:array, Hash.Full}) + + belongs_to(:transaction, Transaction, + foreign_key: :hash, + primary_key: true, + references: :hash, + type: Hash.Full, + define_field: false + ) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = struct, attrs \\ %{}) do + struct + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:hash) + |> unique_constraint(:hash) + end +end diff --git a/apps/explorer/lib/explorer/chain/beacon/reader.ex b/apps/explorer/lib/explorer/chain/beacon/reader.ex new file mode 100644 index 000000000000..5fb8189ec585 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/beacon/reader.ex @@ -0,0 +1,216 @@ +defmodule Explorer.Chain.Beacon.Reader do + @moduledoc "Contains read functions for beacon chain related modules." + + import Ecto.Query, + only: [ + subquery: 1, + distinct: 3, + from: 2, + limit: 2, + order_by: 3, + where: 2, + where: 3, + join: 5, + select: 3, + select_merge: 3 + ] + + import Explorer.Chain, only: [select_repo: 1] + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Block, DenormalizationHelper, Hash, Transaction} + alias Explorer.Chain.Beacon.{Blob, BlobTransaction} + + @doc """ + Finds `t:Explorer.Chain.Beacon.Blob.t/0` by its `hash`. + + Returns `{:ok, %Explorer.Chain.Beacon.Blob{}}` if found + + iex> %Explorer.Chain.Beacon.Blob{hash: hash} = insert(:blob) + iex> {:ok, %Explorer.Chain.Beacon.Blob{hash: found_hash}} = Explorer.Chain.Beacon.Reader.blob(hash, true) + iex> found_hash == hash + true + + Returns `{:error, :not_found}` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash( + ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" + ...> ) + iex> Explorer.Chain.Beacon.Reader.blob(hash, true) + {:error, :not_found} + + """ + @spec blob(Hash.Full.t(), boolean(), [Chain.api?()]) :: {:error, :not_found} | {:ok, Blob.t()} + def blob(hash, with_data, options \\ []) when is_list(options) do + query = + if with_data do + Blob + |> where(hash: ^hash) + else + Blob + |> where(hash: ^hash) + |> select_merge([_], %{blob_data: nil}) + end + + query + |> select_repo(options).one() + |> case do + nil -> {:error, :not_found} + blob -> {:ok, blob} + end + end + + @doc """ + Finds all `t:Explorer.Chain.Beacon.Blob.t/0`s for `t:Explorer.Chain.Transaction.t/0`. + + Returns a list of `%Explorer.Chain.Beacon.Blob{}` belonging to the given `transaction_hash`. + + iex> blob = insert(:blob) + iex> %Explorer.Chain.Beacon.BlobTransaction{hash: transaction_hash} = insert(:blob_transaction, blob_versioned_hashes: [blob.hash]) + iex> blobs = Explorer.Chain.Beacon.Reader.transaction_to_blobs(transaction_hash) + iex> blobs == [%{hash: blob.hash, blob_data: blob.blob_data, kzg_commitment: blob.kzg_commitment, kzg_proof: blob.kzg_proof}] + true + + """ + @spec transaction_to_blobs(Hash.Full.t(), [Chain.api?()]) :: [Blob.t()] + def transaction_to_blobs(transaction_hash, options \\ []) when is_list(options) do + query = + from( + transaction_blob in subquery( + from( + blob_transaction in BlobTransaction, + select: %{ + hash: fragment("unnest(blob_versioned_hashes)"), + idx: fragment("generate_series(1, array_length(blob_versioned_hashes, 1))") + }, + where: blob_transaction.hash == ^transaction_hash + ) + ), + left_join: blob in Blob, + on: blob.hash == transaction_blob.hash, + select: %{ + hash: type(transaction_blob.hash, Hash.Full), + blob_data: blob.blob_data, + kzg_commitment: blob.kzg_commitment, + kzg_proof: blob.kzg_proof + }, + order_by: transaction_blob.idx + ) + + query + |> select_repo(options).all() + end + + @doc """ + Finds associated transaction hashes for the given blob `hash` identifier. Returns at most 10 matches. + + Returns a list of `%{block_consensus: boolean(), transaction_hash: Hash.Full.t()}` maps for all found transactions. + + iex> %Explorer.Chain.Beacon.Blob{hash: blob_hash} = insert(:blob) + iex> %Explorer.Chain.Beacon.BlobTransaction{hash: transaction_hash} = insert(:blob_transaction, blob_versioned_hashes: [blob_hash]) + iex> blob_transactions = Explorer.Chain.Beacon.Reader.blob_hash_to_transactions(blob_hash) + iex> blob_transactions == [%{block_consensus: true, transaction_hash: transaction_hash}] + true + """ + @spec blob_hash_to_transactions(Hash.Full.t(), [Chain.api?()]) :: [ + %{ + block_consensus: boolean(), + transaction_hash: Hash.Full.t() + } + ] + def blob_hash_to_transactions(hash, options \\ []) when is_list(options) do + query = + BlobTransaction + |> where(type(^hash, Hash.Full) == fragment("any(blob_versioned_hashes)")) + |> join(:inner, [bt], transaction in Transaction, on: bt.hash == transaction.hash) + |> limit(10) + + query_with_denormalization = + if DenormalizationHelper.transactions_denormalization_finished?() do + query + |> order_by([bt, transaction], desc: transaction.block_consensus, desc: transaction.block_number) + |> select([bt, transaction], %{ + block_consensus: transaction.block_consensus, + transaction_hash: transaction.hash + }) + else + query + |> join(:inner, [bt, transaction], block in Block, on: block.hash == transaction.block_hash) + |> order_by([bt, transaction, block], desc: block.consensus, desc: transaction.block_number) + |> select([bt, transaction, block], %{ + block_consensus: block.consensus, + transaction_hash: transaction.hash + }) + end + + query_with_denormalization |> select_repo(options).all() + end + + @doc """ + Returns a stream of all unique block timestamps containing missing data blobs. + Filters blocks by `min_block` and `max_block` if provided. + """ + @spec stream_missed_blob_transactions_timestamps( + initial :: accumulator, + reducer :: (entry :: DateTime.t(), accumulator -> accumulator), + min_block :: integer() | nil, + max_block :: integer() | nil, + options :: [] + ) :: {:ok, accumulator} + when accumulator: term() + def stream_missed_blob_transactions_timestamps(initial, reducer, min_block, max_block, options \\ []) + when is_list(options) do + query = + from( + transaction_blob in subquery( + from( + blob_transaction in BlobTransaction, + select: %{ + transaction_hash: blob_transaction.hash, + blob_hash: fragment("unnest(blob_versioned_hashes)") + } + ) + ), + inner_join: transaction in Transaction, + on: transaction_blob.transaction_hash == transaction.hash, + # EIP-2718 blob transaction type + where: transaction.type == 3, + left_join: blob in Blob, + on: blob.hash == transaction_blob.blob_hash, + where: is_nil(blob.hash) + ) + + query_with_denormalization = + if DenormalizationHelper.transactions_denormalization_finished?() do + query + |> distinct([transaction_blob, transaction, blob], transaction.block_timestamp) + |> select([transaction_blob, transaction, blob], transaction.block_timestamp) + else + query + |> join(:inner, [transaction_blob, transaction, blob], block in Block, on: block.hash == transaction.block_hash) + |> distinct([transaction_blob, transaction, blob, block], block.timestamp) + |> select([transaction_blob, transaction, blob, block], block.timestamp) + end + + query_with_denormalization + |> add_min_block_filter(min_block) + |> add_max_block_filter(max_block) + |> Repo.stream_reduce(initial, reducer) + end + + defp add_min_block_filter(query, block_number) do + if is_integer(block_number) do + query |> where([_, transaction], transaction.block_number >= ^block_number) + else + query + end + end + + defp add_max_block_filter(query, block_number) do + if is_integer(block_number) and block_number > 0 do + query |> where([_, transaction], transaction.block_number <= ^block_number) + else + query + end + end +end diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index a1ab2d19e6a8..395f3ada7044 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -1,3 +1,93 @@ +defmodule Explorer.Chain.Block.Schema do + @moduledoc false + + alias Explorer.Chain.{Address, Block, Hash, PendingBlockOperation, Transaction, Wei, Withdrawal} + alias Explorer.Chain.Block.{Reward, SecondDegreeRelation} + alias Explorer.Chain.ZkSync.BatchBlock, as: ZkSyncBatchBlock + + @chain_type_fields (case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> + elem( + quote do + field(:blob_gas_used, :decimal) + field(:excess_blob_gas, :decimal) + end, + 2 + ) + + "rsk" -> + elem( + quote do + field(:bitcoin_merged_mining_header, :binary) + field(:bitcoin_merged_mining_coinbase_transaction, :binary) + field(:bitcoin_merged_mining_merkle_proof, :binary) + field(:hash_for_merged_mining, :binary) + field(:minimum_gas_price, :decimal) + end, + 2 + ) + + "zksync" -> + elem( + quote do + has_one(:zksync_batch_block, ZkSyncBatchBlock, foreign_key: :hash, references: :hash) + has_one(:zksync_batch, through: [:zksync_batch_block, :batch]) + has_one(:zksync_commit_transaction, through: [:zksync_batch, :commit_transaction]) + has_one(:zksync_prove_transaction, through: [:zksync_batch, :prove_transaction]) + has_one(:zksync_execute_transaction, through: [:zksync_batch, :execute_transaction]) + end, + 2 + ) + + _ -> + [] + end) + + defmacro generate do + quote do + @primary_key false + typed_schema "blocks" do + field(:hash, Hash.Full, primary_key: true, null: false) + field(:consensus, :boolean, null: false) + field(:difficulty, :decimal) + field(:gas_limit, :decimal, null: false) + field(:gas_used, :decimal, null: false) + field(:nonce, Hash.Nonce, null: false) + field(:number, :integer, null: false) + field(:size, :integer) + field(:timestamp, :utc_datetime_usec, null: false) + field(:total_difficulty, :decimal) + field(:refetch_needed, :boolean) + field(:base_fee_per_gas, Wei) + field(:is_empty, :boolean) + + timestamps() + + belongs_to(:miner, Address, foreign_key: :miner_hash, references: :hash, type: Hash.Address, null: false) + + has_many(:nephew_relations, SecondDegreeRelation, foreign_key: :uncle_hash, references: :hash) + has_many(:nephews, through: [:nephew_relations, :nephew], references: :hash) + + belongs_to(:parent, Block, foreign_key: :parent_hash, references: :hash, type: Hash.Full, null: false) + + has_many(:uncle_relations, SecondDegreeRelation, foreign_key: :nephew_hash, references: :hash) + has_many(:uncles, through: [:uncle_relations, :uncle], references: :hash) + + has_many(:transactions, Transaction, references: :hash) + has_many(:transaction_forks, Transaction.Fork, foreign_key: :uncle_hash, references: :hash) + + has_many(:rewards, Reward, foreign_key: :block_hash, references: :hash) + + has_many(:withdrawals, Withdrawal, foreign_key: :block_hash, references: :hash) + + has_one(:pending_operations, PendingBlockOperation, foreign_key: :block_hash, references: :hash) + + unquote_splicing(@chain_type_fields) + end + end + end +end + defmodule Explorer.Chain.Block do @moduledoc """ A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally @@ -5,12 +95,27 @@ defmodule Explorer.Chain.Block do structure that they form is called a "blockchain". """ + require Explorer.Chain.Block.Schema + use Explorer.Schema - alias Explorer.Chain.{Address, Block, Gas, Hash, PendingBlockOperation, Transaction, Wei, Withdrawal} - alias Explorer.Chain.Block.{Reward, SecondDegreeRelation} + alias Explorer.Chain.{Block, Hash, Transaction, Wei} + alias Explorer.Chain.Block.{EmissionReward, Reward} + alias Explorer.Repo @optional_attrs ~w(size refetch_needed total_difficulty difficulty base_fee_per_gas)a + |> (&(case Application.compile_env(:explorer, :chain_type) do + "rsk" -> + &1 ++ + ~w(minimum_gas_price bitcoin_merged_mining_header bitcoin_merged_mining_coinbase_transaction bitcoin_merged_mining_merkle_proof hash_for_merged_mining)a + + "ethereum" -> + &1 ++ + ~w(blob_gas_used excess_blob_gas)a + + _ -> + &1 + end)).() @required_attrs ~w(consensus gas_limit gas_used hash miner_hash nonce number parent_hash timestamp)a @@ -47,63 +152,22 @@ defmodule Explorer.Chain.Block do * `total_difficulty` - the total `difficulty` of the chain until this block. * `transactions` - the `t:Explorer.Chain.Transaction.t/0` in this block. * `base_fee_per_gas` - Minimum fee required per unit of gas. Fee adjusts based on network congestion. + #{case Application.compile_env(:explorer, :chain_type) do + "rsk" -> """ + * `bitcoin_merged_mining_header` - Bitcoin merged mining header on Rootstock chains. + * `bitcoin_merged_mining_coinbase_transaction` - Bitcoin merged mining coinbase transaction on Rootstock chains. + * `bitcoin_merged_mining_merkle_proof` - Bitcoin merged mining merkle proof on Rootstock chains. + * `hash_for_merged_mining` - Hash for merged mining on Rootstock chains. + * `minimum_gas_price` - Minimum block gas price on Rootstock chains. + """ + "ethereum" -> """ + * `blob_gas_used` - The total amount of blob gas consumed by the transactions within the block. + * `excess_blob_gas` - The running total of blob gas consumed in excess of the target, prior to the block. + """ + _ -> "" + end} """ - @type t :: %__MODULE__{ - consensus: boolean(), - difficulty: difficulty(), - gas_limit: Gas.t(), - gas_used: Gas.t(), - hash: Hash.Full.t(), - miner: %Ecto.Association.NotLoaded{} | Address.t(), - miner_hash: Hash.Address.t(), - nonce: Hash.Nonce.t(), - number: block_number(), - parent_hash: Hash.t(), - size: non_neg_integer(), - timestamp: DateTime.t(), - total_difficulty: difficulty(), - transactions: %Ecto.Association.NotLoaded{} | [Transaction.t()], - refetch_needed: boolean(), - base_fee_per_gas: Wei.t(), - is_empty: boolean() - } - - @primary_key {:hash, Hash.Full, autogenerate: false} - schema "blocks" do - field(:consensus, :boolean) - field(:difficulty, :decimal) - field(:gas_limit, :decimal) - field(:gas_used, :decimal) - field(:nonce, Hash.Nonce) - field(:number, :integer) - field(:size, :integer) - field(:timestamp, :utc_datetime_usec) - field(:total_difficulty, :decimal) - field(:refetch_needed, :boolean) - field(:base_fee_per_gas, Wei) - field(:is_empty, :boolean) - - timestamps() - - belongs_to(:miner, Address, foreign_key: :miner_hash, references: :hash, type: Hash.Address) - - has_many(:nephew_relations, SecondDegreeRelation, foreign_key: :uncle_hash) - has_many(:nephews, through: [:nephew_relations, :nephew]) - - belongs_to(:parent, __MODULE__, foreign_key: :parent_hash, references: :hash, type: Hash.Full) - - has_many(:uncle_relations, SecondDegreeRelation, foreign_key: :nephew_hash) - has_many(:uncles, through: [:uncle_relations, :uncle]) - - has_many(:transactions, Transaction) - has_many(:transaction_forks, Transaction.Fork, foreign_key: :uncle_hash) - - has_many(:rewards, Reward, foreign_key: :block_hash) - - has_many(:withdrawals, Withdrawal, foreign_key: :block_hash) - - has_one(:pending_operations, PendingBlockOperation, foreign_key: :block_hash) - end + Explorer.Chain.Block.Schema.generate() def changeset(%__MODULE__{} = block, attrs) do block @@ -159,4 +223,201 @@ defmodule Explorer.Chain.Block do end def block_type_filter(query, "Uncle"), do: where(query, [block], block.consensus == false) + + @doc """ + Returns query that fetches up to `limit` of consensus blocks + that are missing rootstock data ordered by number desc. + """ + @spec blocks_without_rootstock_data_query(non_neg_integer()) :: Ecto.Query.t() + def blocks_without_rootstock_data_query(limit) do + from( + block in __MODULE__, + where: + is_nil(block.minimum_gas_price) or + is_nil(block.bitcoin_merged_mining_header) or + is_nil(block.bitcoin_merged_mining_coinbase_transaction) or + is_nil(block.bitcoin_merged_mining_merkle_proof) or + is_nil(block.hash_for_merged_mining), + where: block.consensus == true, + limit: ^limit, + order_by: [desc: block.number] + ) + end + + @doc """ + Calculates transaction fees (gas price * gas used) for the list of transactions (from a single block) + """ + @spec transaction_fees([Transaction.t()]) :: Decimal.t() + def transaction_fees(transactions) do + Enum.reduce(transactions, Decimal.new(0), fn %{gas_used: gas_used, gas_price: gas_price}, acc -> + if gas_price do + gas_used + |> Decimal.new() + |> Decimal.mult(gas_price_to_decimal(gas_price)) + |> Decimal.add(acc) + else + acc + end + end) + end + + @doc """ + Finds blob transaction gas price for the list of transactions (from a single block) + """ + @spec transaction_blob_gas_price([Transaction.t()]) :: Decimal.t() | nil + def transaction_blob_gas_price(transactions) do + transactions + |> Enum.find_value(fn %{beacon_blob_transaction: beacon_blob_transaction} -> + if is_nil(beacon_blob_transaction) do + nil + else + gas_price_to_decimal(beacon_blob_transaction.blob_gas_price) + end + end) + end + + defp gas_price_to_decimal(nil), do: nil + defp gas_price_to_decimal(%Wei{} = wei), do: wei.value + defp gas_price_to_decimal(gas_price), do: Decimal.new(gas_price) + + @doc """ + Calculates burnt fees for the list of transactions (from a single block) + """ + @spec burnt_fees(list(), Decimal.t() | nil) :: Decimal.t() + def burnt_fees(transactions, base_fee_per_gas) do + if is_nil(base_fee_per_gas) do + Decimal.new(0) + else + transactions + |> Enum.reduce(Decimal.new(0), fn %{gas_used: gas_used}, acc -> + gas_used + |> Decimal.new() + |> Decimal.add(acc) + end) + |> Decimal.mult(gas_price_to_decimal(base_fee_per_gas)) + end + end + + @uncle_reward_coef 32 + @spec block_reward_by_parts(Block.t(), [Transaction.t()]) :: %{ + block_number: block_number(), + block_hash: Hash.Full.t(), + miner_hash: Hash.Address.t(), + static_reward: any(), + transaction_fees: any(), + burnt_fees: Wei.t() | nil, + uncle_reward: Wei.t() + } + def block_reward_by_parts(block, transactions) do + %{hash: block_hash, number: block_number} = block + base_fee_per_gas = Map.get(block, :base_fee_per_gas) + + transaction_fees = transaction_fees(transactions) + + static_reward = + Repo.one( + from( + er in EmissionReward, + where: fragment("int8range(?, ?) <@ ?", ^block_number, ^(block_number + 1), er.block_range), + select: er.reward + ) + ) || %Wei{value: Decimal.new(0)} + + uncles_count = if is_list(block.uncles), do: Enum.count(block.uncles), else: 0 + + burnt_fees = burnt_fees(transactions, base_fee_per_gas) + uncle_reward = static_reward |> Wei.div(@uncle_reward_coef) |> Wei.mult(uncles_count) + + # eip4844 blob transactions don't impact validator rewards, so we don't count them here as part of transaction_fees and burnt_fees + %{ + block_number: block_number, + block_hash: block_hash, + miner_hash: block.miner_hash, + static_reward: static_reward, + transaction_fees: %Wei{value: transaction_fees}, + burnt_fees: %Wei{value: burnt_fees}, + uncle_reward: uncle_reward + } + end + + def uncle_reward_coef, do: @uncle_reward_coef + + @doc """ + Calculates the gas target for a given block. + + The gas target represents the percentage by which the actual gas used is above or below the gas target for the block, adjusted by the elasticity multiplier. + If the `gas_limit` is greater than 0, it calculates the ratio of `gas_used` to `gas_limit` adjusted by this multiplier. + """ + @spec gas_target(t()) :: float() + def gas_target(block) do + if Decimal.compare(block.gas_limit, 0) == :gt do + elasticity_multiplier = Application.get_env(:explorer, :elasticity_multiplier) + ratio = Decimal.div(block.gas_used, Decimal.div(block.gas_limit, elasticity_multiplier)) + ratio |> Decimal.sub(1) |> Decimal.mult(100) |> Decimal.to_float() + else + 0.0 + end + end + + @doc """ + Calculates the percentage of gas used for a given block relative to its gas limit. + + This function determines what percentage of the block's gas limit was actually used by the transactions in the block. + """ + @spec gas_used_percentage(t()) :: float() + def gas_used_percentage(block) do + if Decimal.compare(block.gas_limit, 0) == :gt do + block.gas_used |> Decimal.div(block.gas_limit) |> Decimal.mult(100) |> Decimal.to_float() + else + 0.0 + end + end + + @doc """ + Calculates the base fee for the next block based on the current block's gas usage. + + The base fee calculation uses the following [formula](https://eips.ethereum.org/EIPS/eip-1559): + + gas_target = gas_limit / elasticity_multiplier + base_fee_for_next_block = base_fee_per_gas + (base_fee_per_gas * gas_used_delta / gas_target / base_fee_max_change_denominator) + + where elasticity_multiplier is an env variable `EIP_1559_ELASTICITY_MULTIPLIER`, + `gas_used_delta` is the difference between the actual gas used and the target gas + and `base_fee_max_change_denominator` is an env variable `EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR` that limits the maximum change of the base fee from one block to the next. + + + """ + @spec next_block_base_fee_per_gas :: Decimal.t() | nil + def next_block_base_fee_per_gas do + query = + from(block in Block, + where: block.consensus == true, + order_by: [desc: block.number], + limit: 1 + ) + + case Repo.one(query) do + nil -> nil + block -> next_block_base_fee_per_gas(block) + end + end + + @spec next_block_base_fee_per_gas(t()) :: Decimal.t() | nil + def next_block_base_fee_per_gas(block) do + elasticity_multiplier = Application.get_env(:explorer, :elasticity_multiplier) + base_fee_max_change_denominator = Application.get_env(:explorer, :base_fee_max_change_denominator) + + gas_target = Decimal.div(block.gas_limit, elasticity_multiplier) + + gas_used_delta = Decimal.sub(block.gas_used, gas_target) + + base_fee_per_gas_decimal = block.base_fee_per_gas |> Wei.to(:wei) + + base_fee_per_gas_decimal && + base_fee_per_gas_decimal + |> Decimal.mult(gas_used_delta) + |> Decimal.div(gas_target) + |> Decimal.div(base_fee_max_change_denominator) + |> Decimal.add(base_fee_per_gas_decimal) + end end diff --git a/apps/explorer/lib/explorer/chain/block/emission_reward.ex b/apps/explorer/lib/explorer/chain/block/emission_reward.ex index a6c3713fcce0..8be68584b56e 100644 --- a/apps/explorer/lib/explorer/chain/block/emission_reward.ex +++ b/apps/explorer/lib/explorer/chain/block/emission_reward.ex @@ -5,7 +5,7 @@ defmodule Explorer.Chain.Block.EmissionReward do use Explorer.Schema - alias Explorer.Chain.Block.{EmissionReward, Range} + alias Explorer.Chain.Block.Range alias Explorer.Chain.Wei @typedoc """ @@ -14,15 +14,10 @@ defmodule Explorer.Chain.Block.EmissionReward do * `:block_range` - Range of block numbers * `:reward` - Reward given in Wei """ - @type t :: %EmissionReward{ - block_range: Range.t(), - reward: Wei.t() - } - @primary_key false - schema "emission_rewards" do - field(:block_range, Range) - field(:reward, Wei) + typed_schema "emission_rewards" do + field(:block_range, Range, null: false) + field(:reward, Wei, null: false) end def changeset(%__MODULE__{} = emission_reward, attrs) do diff --git a/apps/explorer/lib/explorer/chain/block/reward.ex b/apps/explorer/lib/explorer/chain/block/reward.ex index fd6296a550ea..f558c0eced42 100644 --- a/apps/explorer/lib/explorer/chain/block/reward.ex +++ b/apps/explorer/lib/explorer/chain/block/reward.ex @@ -5,13 +5,12 @@ defmodule Explorer.Chain.Block.Reward do use Explorer.Schema - import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - alias Explorer.Application.Constants - alias Explorer.{Chain, PagingOptions} + alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.Block.Reward.AddressType alias Explorer.Chain.{Address, Block, Hash, Validator, Wei} alias Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand + alias Explorer.Chain.SmartContract alias Explorer.SmartContract.Reader @required_attrs ~w(address_hash address_type block_hash reward)a @@ -44,26 +43,18 @@ defmodule Explorer.Chain.Block.Reward do * `:block_hash` - Hash of the validated block * `:reward` - Total block reward """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - address_hash: Hash.Address.t(), - address_type: AddressType.t(), - block: %Ecto.Association.NotLoaded{} | Block.t() | nil, - block_hash: Hash.Full.t(), - reward: Wei.t() - } - @primary_key false - schema "block_rewards" do - field(:address_type, AddressType) - field(:reward, Wei) + typed_schema "block_rewards" do + field(:address_type, AddressType, null: false) + field(:reward, Wei, null: false) belongs_to( :address, Address, foreign_key: :address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false ) belongs_to( @@ -71,7 +62,8 @@ defmodule Explorer.Chain.Block.Reward do Block, foreign_key: :block_hash, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) timestamps() @@ -144,7 +136,7 @@ defmodule Explorer.Chain.Block.Reward do end end - defp is_validator(mining_key) do + defp validator?(mining_key) do validators_contract_address = Application.get_env(:explorer, Explorer.Chain.Block.Reward, %{})[:validators_contract_address] @@ -192,7 +184,7 @@ defmodule Explorer.Chain.Block.Reward do end def get_validator_payout_key_by_mining(mining_key) do - is_validator = is_validator(mining_key) + is_validator = validator?(mining_key) if is_validator do keys_manager_contract_address = @@ -218,7 +210,7 @@ defmodule Explorer.Chain.Block.Reward do payout_key_hash = call_contract(keys_manager_contract_address, @get_payout_by_mining_abi, get_payout_by_mining_params) - if payout_key_hash == burn_address_hash_string() do + if payout_key_hash == SmartContract.burn_address_hash_string() do mining_key else choose_key(payout_key_hash, mining_key) @@ -248,7 +240,7 @@ defmodule Explorer.Chain.Block.Reward do case Reader.query_contract(address, abi, params, false) do %{^method_id => {:ok, [result]}} -> result - _ -> burn_address_hash_string() + _ -> SmartContract.burn_address_hash_string() end end @@ -279,4 +271,14 @@ defmodule Explorer.Chain.Block.Reward do query end end + + @doc """ + Checks if an address has rewards + """ + @spec address_has_rewards?(Hash.Address.t()) :: boolean() + def address_has_rewards?(address_hash) do + query = from(r in __MODULE__, where: r.address_hash == ^address_hash) + + Repo.exists?(query) + end end diff --git a/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex b/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex index b9a2af6de16a..941dc51480e7 100644 --- a/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex +++ b/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex @@ -8,7 +8,7 @@ defmodule Explorer.Chain.Block.SecondDegreeRelation do Uncles occur when a Proof-of-Work proof is completed slightly late, but before the next block is completes, so the network knows about the late proof and can credit as an uncle in the next block. - This schema is the join schema between the `nephew` and the `uncle` it is is including the `uncle`. The actual + This schema is the join schema between the `nephew` and the `uncle` it is including the `uncle`. The actual `uncle` block is still a normal `t:Explorer.Chain.Block.t/0`. """ @@ -29,31 +29,26 @@ defmodule Explorer.Chain.Block.SecondDegreeRelation do * `uncle_hash` - foreign key for `uncle`. * `index` - index of the uncle within its nephew. Can be `nil` for blocks fetched before this field was added. """ - @type t :: - %__MODULE__{ - nephew: %Ecto.Association.NotLoaded{} | Block.t(), - nephew_hash: Hash.Full.t(), - uncle: %Ecto.Association.NotLoaded{} | Block.t() | nil, - uncle_fetched_at: nil, - uncle_hash: Hash.Full.t(), - index: non_neg_integer() | nil - } - | %__MODULE__{ - nephew: %Ecto.Association.NotLoaded{} | Block.t(), - nephew_hash: Hash.Full.t(), - uncle: %Ecto.Association.NotLoaded{} | Block.t(), - uncle_fetched_at: DateTime.t(), - uncle_hash: Hash.Full.t(), - index: non_neg_integer() | nil - } - @primary_key false - schema "block_second_degree_relations" do + typed_schema "block_second_degree_relations" do field(:uncle_fetched_at, :utc_datetime_usec) - field(:index, :integer) - - belongs_to(:nephew, Block, foreign_key: :nephew_hash, primary_key: true, references: :hash, type: Hash.Full) - belongs_to(:uncle, Block, foreign_key: :uncle_hash, primary_key: true, references: :hash, type: Hash.Full) + field(:index, :integer, null: true) + + belongs_to(:nephew, Block, + foreign_key: :nephew_hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) + + belongs_to(:uncle, Block, + foreign_key: :uncle_hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) end def changeset(%__MODULE__{} = uncle, params) do diff --git a/apps/explorer/lib/explorer/chain/block_number_helper.ex b/apps/explorer/lib/explorer/chain/block_number_helper.ex new file mode 100644 index 000000000000..42da4d376ed7 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/block_number_helper.ex @@ -0,0 +1,25 @@ +# credo:disable-for-this-file +defmodule Explorer.Chain.BlockNumberHelper do + @moduledoc """ + Functions to operate with block numbers based on null round heights (applicable for CHAIN_TYPE=filecoin) + """ + + def previous_block_number(number), do: neighbor_block_number(number, :previous) + + def next_block_number(number), do: neighbor_block_number(number, :next) + + case Application.compile_env(:explorer, :chain_type) do + "filecoin" -> + def null_rounds_count, do: Explorer.Chain.NullRoundHeight.total() + + defp neighbor_block_number(number, direction), + do: Explorer.Chain.NullRoundHeight.neighbor_block_number(number, direction) + + _ -> + def null_rounds_count, do: 0 + defp neighbor_block_number(number, direction), do: move_by_one(number, direction) + end + + def move_by_one(number, :previous), do: number - 1 + def move_by_one(number, :next), do: number + 1 +end diff --git a/apps/explorer/lib/explorer/chain/bridged_token.ex b/apps/explorer/lib/explorer/chain/bridged_token.ex new file mode 100644 index 000000000000..4717d610b9ae --- /dev/null +++ b/apps/explorer/lib/explorer/chain/bridged_token.ex @@ -0,0 +1,1019 @@ +defmodule Explorer.Chain.BridgedToken do + @moduledoc """ + Represents a bridged token. + """ + use Explorer.Schema + + import Ecto.Changeset + import EthereumJSONRPC, only: [json_rpc: 2] + + import Ecto.Query, + only: [ + from: 2, + limit: 2, + where: 2 + ] + + alias ABI.{TypeDecoder, TypeEncoder} + alias Ecto.Changeset + alias EthereumJSONRPC.Contract + alias Explorer.{Chain, PagingOptions, Repo, SortingHelper} + + alias Explorer.Chain.{ + BridgedToken, + Hash, + InternalTransaction, + Search, + Token, + Transaction + } + + require Logger + + @default_paging_options %PagingOptions{page_size: 50} + # keccak 256 from name() + @name_signature "0x06fdde03" + # 95d89b41 = keccak256(symbol()) + @symbol_signature "0x95d89b41" + # keccak 256 from decimals() + @decimals_signature "0x313ce567" + # keccak 256 from totalSupply() + @total_supply_signature "0x18160ddd" + # keccak 256 from token0() + @token0_signature "0x0dfe1681" + # keccak 256 from token1() + @token1_signature "0xd21220a7" + + @derive {Poison.Encoder, + except: [ + :__meta__, + :home_token_contract_address, + :inserted_at, + :updated_at + ]} + + @derive {Jason.Encoder, + except: [ + :__meta__, + :home_token_contract_address, + :inserted_at, + :updated_at + ]} + + @typedoc """ + * `foreign_chain_id` - chain ID of a foreign token + * `foreign_token_contract_address_hash` - Foreign token's contract hash + * `home_token_contract_address` - The `t:Address.t/0` of the home token's contract + * `home_token_contract_address_hash` - Home token's contract hash foreign key + * `custom_metadata` - Arbitrary string with custom metadata. For instance, tokens/weights for Balance tokens + * `custom_cap` - Custom capitalization for this token + * `lp_token` - Boolean flag: LP token or not + * `type` - omni/amb + """ + @primary_key false + typed_schema "bridged_tokens" do + field(:foreign_chain_id, :decimal) + field(:foreign_token_contract_address_hash, Hash.Address) + field(:custom_metadata, :string) + field(:custom_cap, :decimal) + field(:lp_token, :boolean) + field(:type, :string) + field(:exchange_rate, :decimal) + + belongs_to( + :home_token_contract_address, + Token, + foreign_key: :home_token_contract_address_hash, + primary_key: true, + references: :contract_address_hash, + type: Hash.Address, + null: false + ) + + timestamps() + end + + @required_attrs ~w(home_token_contract_address_hash)a + @optional_attrs ~w(foreign_chain_id foreign_token_contract_address_hash custom_metadata custom_cap boolean type exchange_rate)a + + @doc false + def changeset(%BridgedToken{} = bridged_token, params \\ %{}) do + bridged_token + |> cast(params, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:home_token_contract_address) + |> unique_constraint(:home_token_contract_address_hash) + end + + def get_unprocessed_mainnet_lp_tokens_list do + query = + from(bt in BridgedToken, + where: bt.foreign_chain_id == ^1, + where: is_nil(bt.lp_token) or bt.lp_token == true, + select: bt + ) + + query + |> Repo.all() + end + + def necessary_envs_passed? do + config = Application.get_env(:explorer, __MODULE__) + eth_omni_bridge_mediator = config[:eth_omni_bridge_mediator] + bsc_omni_bridge_mediator = config[:bsc_omni_bridge_mediator] + poa_omni_bridge_mediator = config[:poa_omni_bridge_mediator] + + (eth_omni_bridge_mediator && eth_omni_bridge_mediator !== "") || + (bsc_omni_bridge_mediator && bsc_omni_bridge_mediator !== "") || + (poa_omni_bridge_mediator && poa_omni_bridge_mediator !== "") + end + + def enabled? do + Application.get_env(:explorer, __MODULE__)[:enabled] + end + + @doc """ + Returns a list of token addresses `t:Address.t/0`s that don't have an + bridged property revealed. + """ + def unprocessed_token_addresses_to_reveal_bridged_tokens do + query = + from(t in Token, + where: is_nil(t.bridged), + select: t.contract_address_hash + ) + + Repo.stream_reduce(query, [], &[&1 | &2]) + end + + @doc """ + Processes AMB tokens from mediators addresses provided + """ + def process_amb_tokens do + amb_bridge_mediators_var = Application.get_env(:explorer, __MODULE__)[:amb_bridge_mediators] + amb_bridge_mediators = (amb_bridge_mediators_var && String.split(amb_bridge_mediators_var, ",")) || [] + + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + foreign_json_rpc = Application.get_env(:explorer, __MODULE__)[:foreign_json_rpc] + + eth_call_foreign_json_rpc_named_arguments = + compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + + try do + amb_bridge_mediators + |> Enum.each(fn amb_bridge_mediator_hash -> + with {:ok, bridge_contract_hash_resp} <- + get_bridge_contract_hash(amb_bridge_mediator_hash, json_rpc_named_arguments), + bridge_contract_hash <- decode_contract_address_hash_response(bridge_contract_hash_resp), + {:ok, destination_chain_id_resp} <- + get_destination_chain_id(bridge_contract_hash, json_rpc_named_arguments), + foreign_chain_id <- decode_contract_integer_response(destination_chain_id_resp), + {:ok, home_token_contract_hash_resp} <- + get_erc677_token_hash(amb_bridge_mediator_hash, json_rpc_named_arguments), + home_token_contract_hash_string <- decode_contract_address_hash_response(home_token_contract_hash_resp), + {:ok, home_token_contract_hash} <- Chain.string_to_address_hash(home_token_contract_hash_string), + {:ok, foreign_mediator_contract_hash_resp} <- + get_foreign_mediator_contract_hash(amb_bridge_mediator_hash, json_rpc_named_arguments), + foreign_mediator_contract_hash <- + decode_contract_address_hash_response(foreign_mediator_contract_hash_resp), + {:ok, foreign_token_contract_hash_resp} <- + get_erc677_token_hash(foreign_mediator_contract_hash, eth_call_foreign_json_rpc_named_arguments), + foreign_token_contract_hash_string <- + decode_contract_address_hash_response(foreign_token_contract_hash_resp), + {:ok, foreign_token_contract_hash} <- Chain.string_to_address_hash(foreign_token_contract_hash_string) do + insert_bridged_token_metadata(home_token_contract_hash, %{ + foreign_chain_id: foreign_chain_id, + foreign_token_address_hash: foreign_token_contract_hash, + custom_metadata: nil, + custom_cap: nil, + lp_token: nil, + type: "amb" + }) + + set_token_bridged_status(home_token_contract_hash, true) + else + result -> + Logger.debug([ + "failed to fetch metadata for token bridged with AMB mediator #{amb_bridge_mediator_hash}", + inspect(result) + ]) + end + end) + rescue + _ -> + :ok + end + + :ok + end + + @doc """ + Fetches bridged tokens metadata from OmniBridge. + """ + def fetch_omni_bridged_tokens_metadata(token_addresses) do + Enum.each(token_addresses, fn token_address_hash -> + created_from_int_tx_success_query = + from( + it in InternalTransaction, + inner_join: t in assoc(it, :transaction), + where: it.created_contract_address_hash == ^token_address_hash, + where: t.status == ^1 + ) + + created_from_int_tx_success = + created_from_int_tx_success_query + |> limit(1) + |> Repo.one() + + created_from_tx_query = + from( + t in Transaction, + where: t.created_contract_address_hash == ^token_address_hash + ) + + created_from_tx = + created_from_tx_query + |> Repo.all() + |> Enum.count() > 0 + + created_from_int_tx_query = + from( + it in InternalTransaction, + where: it.created_contract_address_hash == ^token_address_hash + ) + + created_from_int_tx = + created_from_int_tx_query + |> Repo.all() + |> Enum.count() > 0 + + cond do + created_from_tx -> + set_token_bridged_status(token_address_hash, false) + + created_from_int_tx && !created_from_int_tx_success -> + set_token_bridged_status(token_address_hash, false) + + created_from_int_tx && created_from_int_tx_success -> + proceed_with_set_omni_status(token_address_hash, created_from_int_tx_success) + + true -> + :ok + end + end) + + :ok + end + + defp proceed_with_set_omni_status(token_address_hash, created_from_int_tx_success) do + {:ok, eth_omni_status} = + extract_omni_bridged_token_metadata_wrapper( + token_address_hash, + created_from_int_tx_success, + :eth_omni_bridge_mediator + ) + + {:ok, bsc_omni_status} = + if eth_omni_status do + {:ok, false} + else + extract_omni_bridged_token_metadata_wrapper( + token_address_hash, + created_from_int_tx_success, + :bsc_omni_bridge_mediator + ) + end + + {:ok, poa_omni_status} = + if eth_omni_status || bsc_omni_status do + {:ok, false} + else + extract_omni_bridged_token_metadata_wrapper( + token_address_hash, + created_from_int_tx_success, + :poa_omni_bridge_mediator + ) + end + + if !eth_omni_status && !bsc_omni_status && !poa_omni_status do + set_token_bridged_status(token_address_hash, false) + end + end + + defp extract_omni_bridged_token_metadata_wrapper(token_address_hash, created_from_int_tx_success, mediator) do + omni_bridge_mediator = Application.get_env(:explorer, __MODULE__)[mediator] + %{transaction_hash: transaction_hash} = created_from_int_tx_success + + if omni_bridge_mediator && omni_bridge_mediator !== "" do + {:ok, omni_bridge_mediator_hash} = Chain.string_to_address_hash(omni_bridge_mediator) + + created_by_amb_mediator_query = + from( + it in InternalTransaction, + where: it.transaction_hash == ^transaction_hash, + where: it.to_address_hash == ^omni_bridge_mediator_hash + ) + + created_by_amb_mediator = + created_by_amb_mediator_query + |> Repo.all() + + if Enum.count(created_by_amb_mediator) > 0 do + extract_omni_bridged_token_metadata( + token_address_hash, + omni_bridge_mediator, + omni_bridge_mediator_hash + ) + + {:ok, true} + else + {:ok, false} + end + else + {:ok, false} + end + end + + defp extract_omni_bridged_token_metadata(token_address_hash, omni_bridge_mediator, omni_bridge_mediator_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + with {:ok, _} <- + get_token_interfaces_version_signature(token_address_hash, json_rpc_named_arguments), + {:ok, foreign_token_address_abi_encoded} <- + get_foreign_token_address(omni_bridge_mediator, token_address_hash, json_rpc_named_arguments), + {:ok, bridge_contract_hash_resp} <- + get_bridge_contract_hash(omni_bridge_mediator_hash, json_rpc_named_arguments) do + foreign_token_address_hash_string = decode_contract_address_hash_response(foreign_token_address_abi_encoded) + {:ok, foreign_token_address_hash} = Chain.string_to_address_hash(foreign_token_address_hash_string) + + multi_token_bridge_hash_string = decode_contract_address_hash_response(bridge_contract_hash_resp) + + {:ok, foreign_chain_id_abi_encoded} = + get_destination_chain_id(multi_token_bridge_hash_string, json_rpc_named_arguments) + + foreign_chain_id = decode_contract_integer_response(foreign_chain_id_abi_encoded) + + foreign_json_rpc = Application.get_env(:explorer, __MODULE__)[:foreign_json_rpc] + + custom_metadata = + if foreign_chain_id == 1 do + get_bridged_token_custom_metadata(foreign_token_address_hash, json_rpc_named_arguments, foreign_json_rpc) + else + nil + end + + bridged_token_metadata = %{ + foreign_chain_id: foreign_chain_id, + foreign_token_address_hash: foreign_token_address_hash, + custom_metadata: custom_metadata, + custom_cap: nil, + lp_token: nil, + type: "omni" + } + + insert_bridged_token_metadata(token_address_hash, bridged_token_metadata) + + set_token_bridged_status(token_address_hash, true) + end + end + + defp get_bridge_contract_hash(mediator_hash, json_rpc_named_arguments) do + # keccak 256 from bridgeContract() + bridge_contract_signature = "0xcd596583" + + perform_eth_call_request(bridge_contract_signature, mediator_hash, json_rpc_named_arguments) + end + + defp get_erc677_token_hash(mediator_hash, json_rpc_named_arguments) do + # keccak 256 from erc677token() + erc677_token_signature = "0x18d8f9c9" + + perform_eth_call_request(erc677_token_signature, mediator_hash, json_rpc_named_arguments) + end + + defp get_foreign_mediator_contract_hash(mediator_hash, json_rpc_named_arguments) do + # keccak 256 from mediatorContractOnOtherSide() + mediator_contract_on_other_side_signature = "0x871c0760" + + perform_eth_call_request(mediator_contract_on_other_side_signature, mediator_hash, json_rpc_named_arguments) + end + + defp get_destination_chain_id(bridge_contract_hash, json_rpc_named_arguments) do + # keccak 256 from destinationChainId() + destination_chain_id_signature = "0xb0750611" + + perform_eth_call_request(destination_chain_id_signature, bridge_contract_hash, json_rpc_named_arguments) + end + + defp get_token_interfaces_version_signature(token_address_hash, json_rpc_named_arguments) do + # keccak 256 from getTokenInterfacesVersion() + get_token_interfaces_version_signature = "0x859ba28c" + + perform_eth_call_request(get_token_interfaces_version_signature, token_address_hash, json_rpc_named_arguments) + end + + defp get_foreign_token_address(omni_bridge_mediator, token_address_hash, json_rpc_named_arguments) do + # keccak 256 from foreignTokenAddress(address) + foreign_token_address_signature = "0x47ac7d6a" + + token_address_hash_abi_encoded = + [token_address_hash.bytes] + |> TypeEncoder.encode([:address]) + |> Base.encode16() + + foreign_token_address_method = foreign_token_address_signature <> token_address_hash_abi_encoded + + perform_eth_call_request(foreign_token_address_method, omni_bridge_mediator, json_rpc_named_arguments) + end + + defp perform_eth_call_request(method, destination, json_rpc_named_arguments) + when not is_nil(json_rpc_named_arguments) do + method + |> Contract.eth_call_request(destination, 1, nil, nil) + |> json_rpc(json_rpc_named_arguments) + end + + defp perform_eth_call_request(_method, _destination, json_rpc_named_arguments) + when is_nil(json_rpc_named_arguments) do + :error + end + + def decode_contract_address_hash_response(resp) do + case resp do + "0x000000000000000000000000" <> address -> + "0x" <> address + + _ -> + nil + end + end + + def decode_contract_integer_response(resp) do + case resp do + "0x" <> integer_encoded -> + {integer_value, _} = Integer.parse(integer_encoded, 16) + integer_value + + _ -> + nil + end + end + + defp set_token_bridged_status(token_address_hash, status) do + case Repo.get(Token, token_address_hash) do + %{bridged: bridged} = target_token -> + if !bridged do + token = Changeset.change(target_token, bridged: status) + + Repo.update(token) + end + + _ -> + :ok + end + end + + defp insert_bridged_token_metadata(token_address_hash, %{ + foreign_chain_id: foreign_chain_id, + foreign_token_address_hash: foreign_token_address_hash, + custom_metadata: custom_metadata, + custom_cap: custom_cap, + lp_token: lp_token, + type: type + }) do + target_token = Repo.get(Token, token_address_hash) + + if target_token do + {:ok, _} = + Repo.insert( + %BridgedToken{ + home_token_contract_address_hash: token_address_hash, + foreign_chain_id: foreign_chain_id, + foreign_token_contract_address_hash: foreign_token_address_hash, + custom_metadata: custom_metadata, + custom_cap: custom_cap, + lp_token: lp_token, + type: type + }, + on_conflict: :nothing + ) + end + end + + # Fetches custom metadata for bridged tokens from the node. + # Currently, gets Balancer token composite tokens with their weights + # from foreign chain + defp get_bridged_token_custom_metadata(foreign_token_address_hash, json_rpc_named_arguments, foreign_json_rpc) + when not is_nil(foreign_json_rpc) and foreign_json_rpc !== "" do + eth_call_foreign_json_rpc_named_arguments = + compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + + balancer_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) || + sushiswap_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) + end + + defp get_bridged_token_custom_metadata(_foreign_token_address_hash, _json_rpc_named_arguments, foreign_json_rpc) + when is_nil(foreign_json_rpc) do + nil + end + + defp get_bridged_token_custom_metadata(_foreign_token_address_hash, _json_rpc_named_arguments, foreign_json_rpc) + when foreign_json_rpc == "" do + nil + end + + defp balancer_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) do + # keccak 256 from getCurrentTokens() + get_current_tokens_signature = "0xcc77828d" + + case get_current_tokens_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + {:ok, "0x"} -> + nil + + {:ok, "0x" <> balancer_current_tokens_encoded} -> + [balancer_current_tokens] = + try do + balancer_current_tokens_encoded + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([{:array, :address}]) + rescue + _ -> [] + end + + bridged_token_custom_metadata = + parse_bridged_token_custom_metadata( + balancer_current_tokens, + eth_call_foreign_json_rpc_named_arguments, + foreign_token_address_hash + ) + + tokens_and_weights(bridged_token_custom_metadata) + + _ -> + nil + end + end + + defp tokens_and_weights(bridged_token_custom_metadata) do + with true <- is_map(bridged_token_custom_metadata), + tokens = Map.get(bridged_token_custom_metadata, :tokens), + weights = Map.get(bridged_token_custom_metadata, :weights), + false <- tokens == "" do + if weights !== "", do: "#{tokens} #{weights}", else: tokens + else + _ -> nil + end + end + + defp sushiswap_custom_metadata(foreign_token_address_hash, eth_call_foreign_json_rpc_named_arguments) do + with {:ok, "0x" <> token0_encoded} <- + @token0_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token1_encoded} <- + @token1_signature + |> Contract.eth_call_request(foreign_token_address_hash, 2, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + token0_hash <- parse_contract_response(token0_encoded, :address), + token1_hash <- parse_contract_response(token1_encoded, :address), + false <- is_nil(token0_hash), + false <- is_nil(token1_hash), + token0_hash_str <- "0x" <> Base.encode16(token0_hash, case: :lower), + token1_hash_str <- "0x" <> Base.encode16(token1_hash, case: :lower), + {:ok, "0x" <> token0_name_encoded} <- + @name_signature + |> Contract.eth_call_request(token0_hash_str, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token1_name_encoded} <- + @name_signature + |> Contract.eth_call_request(token1_hash_str, 2, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token0_symbol_encoded} <- + @symbol_signature + |> Contract.eth_call_request(token0_hash_str, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> token1_symbol_encoded} <- + @symbol_signature + |> Contract.eth_call_request(token1_hash_str, 2, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + token0_name = parse_contract_response(token0_name_encoded, :string, {:bytes, 32}) + token1_name = parse_contract_response(token1_name_encoded, :string, {:bytes, 32}) + token0_symbol = parse_contract_response(token0_symbol_encoded, :string, {:bytes, 32}) + token1_symbol = parse_contract_response(token1_symbol_encoded, :string, {:bytes, 32}) + + "#{token0_name}/#{token1_name} (#{token0_symbol}/#{token1_symbol})" + else + _ -> + nil + end + end + + def calc_lp_tokens_total_liquidity do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + foreign_json_rpc = Application.get_env(:explorer, __MODULE__)[:foreign_json_rpc] + bridged_mainnet_tokens_list = BridgedToken.get_unprocessed_mainnet_lp_tokens_list() + + Enum.each(bridged_mainnet_tokens_list, fn bridged_token -> + case calc_sushiswap_lp_tokens_cap( + bridged_token.home_token_contract_address_hash, + bridged_token.foreign_token_contract_address_hash, + json_rpc_named_arguments, + foreign_json_rpc + ) do + {:ok, new_custom_cap} -> + bridged_token + |> Changeset.change(%{custom_cap: new_custom_cap, lp_token: true}) + |> Repo.update() + + {:error, :not_lp_token} -> + bridged_token + |> Changeset.change(%{lp_token: false}) + |> Repo.update() + end + end) + + Logger.debug(fn -> "Total liquidity fetched for LP tokens" end) + end + + defp calc_sushiswap_lp_tokens_cap( + home_token_contract_address_hash, + foreign_token_address_hash, + json_rpc_named_arguments, + foreign_json_rpc + ) do + eth_call_foreign_json_rpc_named_arguments = + compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + + # keccak 256 from getReserves() + get_reserves_signature = "0x0902f1ac" + + with {:ok, "0x" <> get_reserves_encoded} <- + get_reserves_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> home_token_total_supply_encoded} <- + @total_supply_signature + |> Contract.eth_call_request(home_token_contract_address_hash, 1, nil, nil) + |> json_rpc(json_rpc_named_arguments), + [reserve0, reserve1, _] <- + parse_contract_response(get_reserves_encoded, [{:uint, 112}, {:uint, 112}, {:uint, 32}]), + {:ok, token0_cap_usd} <- + get_lp_token_cap( + home_token_total_supply_encoded, + @token0_signature, + reserve0, + foreign_token_address_hash, + eth_call_foreign_json_rpc_named_arguments + ), + {:ok, token1_cap_usd} <- + get_lp_token_cap( + home_token_total_supply_encoded, + @token1_signature, + reserve1, + foreign_token_address_hash, + eth_call_foreign_json_rpc_named_arguments + ) do + total_lp_cap = Decimal.add(token0_cap_usd, token1_cap_usd) + {:ok, total_lp_cap} + else + _ -> + {:error, :not_lp_token} + end + end + + defp get_lp_token_cap( + home_token_total_supply_encoded, + token_signature, + reserve, + foreign_token_address_hash, + eth_call_foreign_json_rpc_named_arguments + ) do + home_token_total_supply = + home_token_total_supply_encoded + |> parse_contract_response({:uint, 256}) + |> Decimal.new() + + case token_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + {:ok, "0x" <> token_encoded} -> + with token_hash <- parse_contract_response(token_encoded, :address), + false <- is_nil(token_hash), + token_hash_str <- "0x" <> Base.encode16(token_hash, case: :lower), + {:ok, "0x" <> token_decimals_encoded} <- + @decimals_signature + |> Contract.eth_call_request(token_hash_str, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments), + {:ok, "0x" <> foreign_token_total_supply_encoded} <- + @total_supply_signature + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + token_decimals = parse_contract_response(token_decimals_encoded, {:uint, 256}) + + foreign_token_total_supply = + foreign_token_total_supply_encoded + |> parse_contract_response({:uint, 256}) + |> Decimal.new() + + token_decimals_divider = + 10 + |> :math.pow(token_decimals) + |> Decimal.from_float() + + token_cap = + reserve + |> Decimal.div(foreign_token_total_supply) + |> Decimal.mult(home_token_total_supply) + |> Decimal.div(token_decimals_divider) + + token = Token.get_by_contract_address_hash(token_hash_str, []) + + token_cap_usd = + if token && token.fiat_value do + token.fiat_value + |> Decimal.mult(token_cap) + else + 0 + end + + {:ok, token_cap_usd} + else + _ -> :error + end + end + end + + defp parse_contract_response(abi_encoded_value, types) when is_list(types) do + values = + try do + abi_encoded_value + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw(types) + rescue + _ -> [nil] + end + + values + end + + defp parse_contract_response(abi_encoded_value, type, emergency_type \\ nil) do + [value] = + try do + [res] = decode_contract_response(abi_encoded_value, type) + + [convert_binary_to_string(res, type)] + rescue + _ -> + if emergency_type do + try do + [res] = decode_contract_response(abi_encoded_value, emergency_type) + + [convert_binary_to_string(res, emergency_type)] + rescue + _ -> + [nil] + end + else + [nil] + end + end + + value + end + + defp decode_contract_response(abi_encoded_value, type) do + abi_encoded_value + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([type]) + end + + defp convert_binary_to_string(binary, type) do + case type do + {:bytes, _} -> + binary_to_string(binary) + + _ -> + binary + end + end + + defp compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc) + when foreign_json_rpc != "" do + {_, eth_call_foreign_json_rpc_named_arguments} = + Keyword.get_and_update(json_rpc_named_arguments, :transport_options, fn transport_options -> + {_, updated_transport_options} = + update_transport_options_set_foreign_json_rpc(transport_options, foreign_json_rpc) + + {transport_options, updated_transport_options} + end) + + eth_call_foreign_json_rpc_named_arguments + end + + defp compose_foreign_json_rpc_named_arguments(_json_rpc_named_arguments, foreign_json_rpc) + when foreign_json_rpc == "" do + nil + end + + defp compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, _foreign_json_rpc) + when is_nil(json_rpc_named_arguments) do + nil + end + + defp update_transport_options_set_foreign_json_rpc(transport_options, foreign_json_rpc) do + Keyword.get_and_update(transport_options, :method_to_url, fn method_to_url -> + {_, updated_method_to_url} = + Keyword.get_and_update(method_to_url, :eth_call, fn eth_call -> + {eth_call, foreign_json_rpc} + end) + + {method_to_url, updated_method_to_url} + end) + end + + defp parse_bridged_token_custom_metadata( + balancer_current_tokens, + eth_call_foreign_json_rpc_named_arguments, + foreign_token_address_hash + ) do + balancer_current_tokens + |> Enum.reduce(%{:tokens => "", :weights => ""}, fn balancer_token_bytes, balancer_tokens_weights -> + balancer_token_hash_without_0x = + balancer_token_bytes + |> Base.encode16(case: :lower) + + balancer_token_hash = "0x" <> balancer_token_hash_without_0x + + case @symbol_signature + |> Contract.eth_call_request(balancer_token_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do + {:ok, "0x" <> symbol_encoded} -> + [symbol] = + symbol_encoded + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([:string]) + + # f1b8a9b7 = keccak256(getNormalizedWeight(address)) + get_normalized_weight_signature = "0xf1b8a9b7" + + get_normalized_weight_arg_abi_encoded = + [balancer_token_bytes] + |> TypeEncoder.encode([:address]) + |> Base.encode16(case: :lower) + + get_normalized_weight_abi_encoded = get_normalized_weight_signature <> get_normalized_weight_arg_abi_encoded + + get_normalized_weight_resp = + get_normalized_weight_abi_encoded + |> Contract.eth_call_request(foreign_token_address_hash, 1, nil, nil) + |> json_rpc(eth_call_foreign_json_rpc_named_arguments) + + parse_balancer_weights(get_normalized_weight_resp, balancer_tokens_weights, symbol) + + _ -> + nil + end + end) + end + + defp parse_balancer_weights(get_normalized_weight_resp, balancer_tokens_weights, symbol) do + case get_normalized_weight_resp do + {:ok, "0x" <> normalized_weight_encoded} -> + [normalized_weight] = + try do + normalized_weight_encoded + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw([{:uint, 256}]) + rescue + _ -> + [] + end + + normalized_weight_to_100_perc = calc_normalized_weight_to_100_perc(normalized_weight) + + normalized_weight_in_perc = + normalized_weight_to_100_perc + |> div(1_000_000_000_000_000_000) + + current_tokens = Map.get(balancer_tokens_weights, :tokens) + current_weights = Map.get(balancer_tokens_weights, :weights) + + tokens_value = combine_tokens_value(current_tokens, symbol) + weights_value = combine_weights_value(current_weights, normalized_weight_in_perc) + + %{:tokens => tokens_value, :weights => weights_value} + + _ -> + nil + end + end + + defp calc_normalized_weight_to_100_perc(normalized_weight) do + if normalized_weight, do: 100 * normalized_weight, else: 0 + end + + defp combine_tokens_value(current_tokens, symbol) do + if current_tokens == "", do: symbol, else: current_tokens <> "/" <> symbol + end + + defp combine_weights_value(current_weights, normalized_weight_in_perc) do + if current_weights == "", + do: "#{normalized_weight_in_perc}", + else: current_weights <> "/" <> "#{normalized_weight_in_perc}" + end + + defp fetch_top_bridged_tokens(chain_ids, paging_options, filter, sorting, options) do + bridged_tokens_query = + __MODULE__ + |> apply_chain_ids_filter(chain_ids) + + base_query = + from(t in Token.base_token_query(nil, sorting), + right_join: bt in subquery(bridged_tokens_query), + on: t.contract_address_hash == bt.home_token_contract_address_hash, + where: t.total_supply > ^0, + where: t.bridged, + select: {t, bt}, + preload: [:contract_address] + ) + + base_query_with_paging = + base_query + |> SortingHelper.page_with_sorting(paging_options, sorting, Token.default_sorting()) + |> limit(^paging_options.page_size) + + query = + if filter && filter !== "" do + case Search.prepare_search_term(filter) do + {:some, filter_term} -> + base_query_with_paging + |> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term)) + + _ -> + base_query_with_paging + end + else + base_query_with_paging + end + + query + |> Chain.select_repo(options).all() + end + + @spec list_top_bridged_tokens(String.t()) :: [{Token.t(), BridgedToken.t()}] + def list_top_bridged_tokens(filter, options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + chain_ids = Keyword.get(options, :chain_ids, nil) + sorting = Keyword.get(options, :sorting, []) + + fetch_top_bridged_tokens(chain_ids, paging_options, filter, sorting, options) + end + + defp apply_chain_ids_filter(query, chain_ids) when chain_ids in [[], nil], do: query + + defp apply_chain_ids_filter(query, chain_ids) when is_list(chain_ids), + do: from(bt in query, where: bt.foreign_chain_id in ^chain_ids) + + def binary_to_string(binary) do + binary + |> :binary.bin_to_list() + |> Enum.filter(fn x -> x != 0 end) + |> List.to_string() + end + + def token_display_name_based_on_bridge_destination(name, foreign_chain_id) do + cond do + Decimal.compare(foreign_chain_id, 1) == :eq -> + name + |> String.replace("on xDai", "from Ethereum") + + Decimal.compare(foreign_chain_id, 56) == :eq -> + name + |> String.replace("on xDai", "from BSC") + + true -> + name + end + end + + def token_display_name_based_on_bridge_destination(name, symbol, foreign_chain_id) do + token_name = + cond do + Decimal.compare(foreign_chain_id, 1) == :eq -> + name + |> String.replace("on xDai", "from Ethereum") + + Decimal.compare(foreign_chain_id, 56) == :eq -> + name + |> String.replace("on xDai", "from BSC") + + true -> + name + end + + "#{token_name} (#{symbol})" + end +end diff --git a/apps/explorer/lib/explorer/chain/cache/addresses_tabs_counters.ex b/apps/explorer/lib/explorer/chain/cache/addresses_tabs_counters.ex index ab79fbf0a5f2..20d30199629a 100644 --- a/apps/explorer/lib/explorer/chain/cache/addresses_tabs_counters.ex +++ b/apps/explorer/lib/explorer/chain/cache/addresses_tabs_counters.ex @@ -20,9 +20,8 @@ defmodule Explorer.Chain.Cache.AddressesTabsCounters do end @spec set_counter(counter_type, String.t(), non_neg_integer()) :: :ok - def set_counter(counter_type, address_hash, counter, need_to_modify_state? \\ true) do + def set_counter(counter_type, address_hash, counter) do :ets.insert(@cache_name, {cache_key(address_hash, counter_type), {DateTime.utc_now(), counter}}) - if need_to_modify_state?, do: ignore_txs(counter_type, address_hash) :ok end @@ -42,10 +41,6 @@ defmodule Explorer.Chain.Cache.AddressesTabsCounters do address_hash |> task_cache_key(counter_type) |> fetch_from_cache(@cache_name, nil) end - @spec ignore_txs(atom, String.t()) :: :ignore | :ok - def ignore_txs(:txs, address_hash), do: GenServer.cast(__MODULE__, {:ignore_txs, address_hash}) - def ignore_txs(_counter_type, _address_hash), do: :ignore - def save_txs_counter_progress(address_hash, results) do GenServer.cast(__MODULE__, {:set_txs_state, address_hash, results}) end @@ -67,16 +62,11 @@ defmodule Explorer.Chain.Cache.AddressesTabsCounters do {:ok, %{}} end - @impl true - def handle_cast({:ignore_txs, address_hash}, state) do - {:noreply, Map.put(state, lowercased_string(address_hash), {:updated, DateTime.utc_now()})} - end - @impl true def handle_cast({:set_txs_state, address_hash, %{txs_types: txs_types} = results}, state) do address_hash = lowercased_string(address_hash) - if is_ignored?(state[address_hash]) do + if ignored?(state[address_hash]) do {:noreply, state} else address_state = @@ -95,24 +85,30 @@ defmodule Explorer.Chain.Cache.AddressesTabsCounters do |> Enum.count() |> min(Counters.counters_limit()) - if counter == Counters.counters_limit() || Enum.count(address_state[:txs_types]) == 3 do - set_counter(:txs, address_hash, counter, false) - {:noreply, Map.put(state, address_hash, {:updated, DateTime.utc_now()})} - else - {:noreply, Map.put(state, address_hash, address_state)} + cond do + Enum.count(address_state[:txs_types]) == 3 -> + set_counter(:txs, address_hash, counter) + {:noreply, Map.put(state, address_hash, nil)} + + counter == Counters.counters_limit() -> + set_counter(:txs, address_hash, counter) + {:noreply, Map.put(state, address_hash, :limit_value)} + + true -> + {:noreply, Map.put(state, address_hash, address_state)} end end end - defp is_ignored?({:updated, datetime}), do: is_up_to_date?(datetime, ttl()) - defp is_ignored?(_), do: false + defp ignored?(:limit_value), do: true + defp ignored?(_), do: false defp check_staleness(nil), do: nil defp check_staleness({datetime, counter}) when counter > 50, do: {datetime, counter, :limit_value} defp check_staleness({datetime, counter}) do status = - if is_up_to_date?(datetime, ttl()) do + if up_to_date?(datetime, ttl()) do :up_to_date else :stale @@ -121,7 +117,7 @@ defmodule Explorer.Chain.Cache.AddressesTabsCounters do {datetime, counter, status} end - defp is_up_to_date?(datetime, ttl) do + defp up_to_date?(datetime, ttl) do datetime |> DateTime.add(ttl, :millisecond) |> DateTime.compare(DateTime.utc_now()) != :lt diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex new file mode 100644 index 000000000000..c4449853b290 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -0,0 +1,55 @@ +defmodule Explorer.Chain.Cache.BackgroundMigrations do + @moduledoc """ + Caches background migrations' status. + """ + + require Logger + + use Explorer.Chain.MapCache, + name: :background_migrations_status, + key: :transactions_denormalization_finished, + key: :tb_token_type_finished, + key: :ctb_token_type_finished, + key: :tt_denormalization_finished + + @dialyzer :no_match + + alias Explorer.Migrator.{ + AddressCurrentTokenBalanceTokenType, + AddressTokenBalanceTokenType, + TokenTransferTokenType, + TransactionsDenormalization + } + + defp handle_fallback(:transactions_denormalization_finished) do + Task.start(fn -> + set_transactions_denormalization_finished(TransactionsDenormalization.migration_finished?()) + end) + + {:return, false} + end + + defp handle_fallback(:tb_token_type_finished) do + Task.start(fn -> + set_tb_token_type_finished(AddressTokenBalanceTokenType.migration_finished?()) + end) + + {:return, false} + end + + defp handle_fallback(:ctb_token_type_finished) do + Task.start(fn -> + set_ctb_token_type_finished(AddressCurrentTokenBalanceTokenType.migration_finished?()) + end) + + {:return, false} + end + + defp handle_fallback(:tt_denormalization_finished) do + Task.start(fn -> + set_tt_denormalization_finished(TokenTransferTokenType.migration_finished?()) + end) + + {:return, false} + end +end diff --git a/apps/explorer/lib/explorer/chain/cache/block.ex b/apps/explorer/lib/explorer/chain/cache/block.ex index e5d7b9ee37ec..0dd216a71bee 100644 --- a/apps/explorer/lib/explorer/chain/cache/block.ex +++ b/apps/explorer/lib/explorer/chain/cache/block.ex @@ -40,9 +40,7 @@ defmodule Explorer.Chain.Cache.Block do |> Decimal.to_integer() if cached_value_from_db === 0 do - count = Helper.estimated_count_from("blocks") - - trunc(count * 0.90) + estimated_count_from_blocks() else cached_value_from_db end @@ -51,6 +49,12 @@ defmodule Explorer.Chain.Cache.Block do end end + defp estimated_count_from_blocks do + count = Helper.estimated_count_from("blocks") + + if is_nil(count), do: 0, else: trunc(count * 0.90) + end + defp handle_fallback(:count) do # This will get the task PID if one exists and launch a new task if not # See next `handle_fallback` definition diff --git a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex index e147c9c0c782..ad2fdb00f941 100644 --- a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex +++ b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex @@ -10,155 +10,284 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do from: 2 ] - alias EthereumJSONRPC.Blocks + alias Explorer.Chain.{Block, Wei} - alias Explorer.Chain.{ - Block, - Wei - } - - alias Explorer.Repo + alias Explorer.Counters.AverageBlockTime + alias Explorer.{Market, Repo} + alias Timex.Duration use Explorer.Chain.MapCache, name: :gas_price, key: :gas_prices, + key: :gas_prices_acc, + key: :updated_at, key: :old_gas_prices, + key: :old_updated_at, key: :async_task, - global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl], + global_ttl: :infinity, ttl_check_interval: :timer.seconds(1), callback: &async_task_on_deletion(&1) @doc """ - Get `safelow`, `average` and `fast` percentile of transactions gas prices among the last `num_of_blocks` blocks + Calculates how much time left till the next gas prices updated taking into account estimated query running time. + """ + @spec update_in :: non_neg_integer() + def update_in do + case {get_old_updated_at(), get_updated_at()} do + {%DateTime{} = old_updated_at, %DateTime{} = updated_at} -> + time_to_update = DateTime.diff(updated_at, old_updated_at, :millisecond) + 500 + time_since_last_update = DateTime.diff(DateTime.utc_now(), updated_at, :millisecond) + next_update_in = time_to_update - time_since_last_update + if next_update_in <= 0, do: global_ttl(), else: next_update_in + + _ -> + global_ttl() + :timer.seconds(2) + end + end + + @doc """ + Calculates the `slow`, `average`, and `fast` gas price and time percentiles from the last `num_of_blocks` blocks and estimates the fiat price for each percentile. + These percentiles correspond to the likelihood of a transaction being picked up by miners depending on the fee offered. """ @spec get_average_gas_price(pos_integer(), pos_integer(), pos_integer(), pos_integer()) :: - {:error, any} | {:ok, %{String.t() => nil | float, String.t() => nil | float, String.t() => nil | float}} + {{:error, any} | {:ok, %{slow: gas_price, average: gas_price, fast: gas_price}}, + [ + %{ + block_number: non_neg_integer(), + slow_gas_price: nil | Decimal.t(), + fast_gas_price: nil | Decimal.t(), + average_gas_price: nil | Decimal.t(), + slow_priority_fee_per_gas: nil | Decimal.t(), + average_priority_fee_per_gas: nil | Decimal.t(), + fast_priority_fee_per_gas: nil | Decimal.t(), + slow_time: nil | Decimal.t(), + average_time: nil | Decimal.t(), + fast_time: nil | Decimal.t() + } + ]} + when gas_price: + nil + | %{ + base_fee: Decimal.t() | nil, + priority_fee: Decimal.t() | nil, + price: float(), + time: float(), + fiat_price: Decimal.t() + } def get_average_gas_price(num_of_blocks, safelow_percentile, average_percentile, fast_percentile) do safelow_percentile_fraction = safelow_percentile / 100 average_percentile_fraction = average_percentile / 100 fast_percentile_fraction = fast_percentile / 100 + acc = get_gas_prices_acc() + + from_block = + case acc do + [%{block_number: from_block} | _] -> from_block + _ -> -1 + end + + average_block_time = + case AverageBlockTime.average_block_time() do + {:error, _} -> nil + average_block_time -> average_block_time |> Duration.to_milliseconds() + end + fee_query = from( block in Block, left_join: transaction in assoc(block, :transactions), where: block.consensus == true, - where: transaction.status == ^1, - where: transaction.gas_price > ^0, - group_by: block.number, - order_by: [desc: block.number], + where: is_nil(transaction.gas_price) or transaction.gas_price > ^0, + where: transaction.block_number > ^from_block, + group_by: transaction.block_number, + order_by: [desc: transaction.block_number], select: %{ + block_number: transaction.block_number, slow_gas_price: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^safelow_percentile_fraction, transaction.gas_price ), average_gas_price: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^average_percentile_fraction, transaction.gas_price ), fast_gas_price: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^fast_percentile_fraction, transaction.gas_price ), - slow: + slow_priority_fee_per_gas: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by least(?, ?) )", ^safelow_percentile_fraction, - transaction.max_priority_fee_per_gas + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - block.base_fee_per_gas ), - average: + average_priority_fee_per_gas: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by least(?, ?) )", ^average_percentile_fraction, - transaction.max_priority_fee_per_gas + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - block.base_fee_per_gas ), - fast: + fast_priority_fee_per_gas: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by least(?, ?) )", ^fast_percentile_fraction, - transaction.max_priority_fee_per_gas + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - block.base_fee_per_gas + ), + slow_time: + fragment( + "percentile_disc(? :: real) within group ( order by coalesce(extract(milliseconds from (?)::interval), ?) desc )", + ^safelow_percentile_fraction, + block.timestamp - transaction.earliest_processing_start, + ^(average_block_time && average_block_time * safelow_time_coefficient()) + ), + average_time: + fragment( + "percentile_disc(? :: real) within group ( order by coalesce(extract(milliseconds from (?)::interval), ?) desc )", + ^average_percentile_fraction, + block.timestamp - transaction.earliest_processing_start, + ^(average_block_time && average_block_time * average_time_coefficient()) + ), + fast_time: + fragment( + "percentile_disc(? :: real) within group ( order by coalesce(extract(milliseconds from (?)::interval), ?) desc )", + ^fast_percentile_fraction, + block.timestamp - transaction.earliest_processing_start, + ^(average_block_time && average_block_time * fast_time_coefficient()) ) }, limit: ^num_of_blocks ) - gas_prices = fee_query |> Repo.all(timeout: :infinity) |> process_fee_data_from_db() + new_acc = fee_query |> Repo.all(timeout: :infinity) |> merge_gas_prices(acc, num_of_blocks) + + gas_prices = new_acc |> process_fee_data_from_db() - {:ok, gas_prices} + {{:ok, gas_prices}, new_acc} catch error -> - {:error, error} + Logger.error("Failed to get gas prices: #{inspect(error)}") + {{:error, error}, get_gas_prices_acc()} end + defp merge_gas_prices(new, acc, acc_size), do: Enum.take(new ++ acc, acc_size) + defp process_fee_data_from_db([]) do %{ - "slow" => nil, - "average" => nil, - "fast" => nil + slow: nil, + average: nil, + fast: nil } end defp process_fee_data_from_db(fees) do - fees_length = Enum.count(fees) - %{ slow_gas_price: slow_gas_price, average_gas_price: average_gas_price, fast_gas_price: fast_gas_price, - slow: slow, - average: average, - fast: fast - } = - fees - |> Enum.reduce( - &Map.merge(&1, &2, fn - _, v1, v2 when nil not in [v1, v2] -> Decimal.add(v1, v2) - _, v1, v2 -> v1 || v2 - end) - ) - |> Map.new(fn - {key, nil} -> {key, nil} - {key, value} -> {key, Decimal.div(value, fees_length)} - end) - - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - {slow_fee, average_fee, fast_fee} = - case {nil not in [slow, average, fast], EthereumJSONRPC.fetch_block_by_tag("pending", json_rpc_named_arguments)} do - {true, {:ok, %Blocks{blocks_params: [%{base_fee_per_gas: base_fee}]}}} when not is_nil(base_fee) -> - base_fee_wei = base_fee |> Decimal.new() |> Wei.from(:wei) + slow_priority_fee_per_gas: slow_priority_fee_per_gas, + average_priority_fee_per_gas: average_priority_fee_per_gas, + fast_priority_fee_per_gas: fast_priority_fee_per_gas, + slow_time: slow_time, + average_time: average_time, + fast_time: fast_time + } = merge_fees(fees) + + {slow_fee, average_fee, fast_fee, base_fee_wei} = + case nil not in [slow_priority_fee_per_gas, average_priority_fee_per_gas, fast_priority_fee_per_gas] && + Block.next_block_base_fee_per_gas() do + %Decimal{} = base_fee -> + base_fee_wei = base_fee |> Wei.from(:wei) { - priority_with_base_fee(slow, base_fee_wei), - priority_with_base_fee(average, base_fee_wei), - priority_with_base_fee(fast, base_fee_wei) + priority_with_base_fee(slow_priority_fee_per_gas, base_fee_wei), + priority_with_base_fee(average_priority_fee_per_gas, base_fee_wei), + priority_with_base_fee(fast_priority_fee_per_gas, base_fee_wei), + base_fee_wei } _ -> - {gas_price(slow_gas_price), gas_price(average_gas_price), gas_price(fast_gas_price)} + {gas_price(slow_gas_price), gas_price(average_gas_price), gas_price(fast_gas_price), nil} end + exchange_rate_from_db = Market.get_coin_exchange_rate() + + %{ + slow: compose_gas_price(slow_fee, slow_time, exchange_rate_from_db, base_fee_wei, slow_priority_fee_per_gas), + average: + compose_gas_price(average_fee, average_time, exchange_rate_from_db, base_fee_wei, average_priority_fee_per_gas), + fast: compose_gas_price(fast_fee, fast_time, exchange_rate_from_db, base_fee_wei, fast_priority_fee_per_gas) + } + end + + defp merge_fees(fees_from_db) do + fees_from_db + |> Stream.map(&Map.delete(&1, :block_number)) + |> Enum.reduce( + &Map.merge(&1, &2, fn + _, nil, nil -> nil + _, val, nil -> [val] + _, nil, acc -> if is_list(acc), do: acc, else: [acc] + _, val, acc -> if is_list(acc), do: [val | acc], else: [val, acc] + end) + ) + |> Map.new(fn + {key, nil} -> + {key, nil} + + {key, value} -> + value = if is_list(value), do: value, else: [value] + count = Enum.count(value) + {key, value |> Enum.reduce(Decimal.new(0), &Decimal.add/2) |> Decimal.div(count)} + end) + end + + defp compose_gas_price(fee, time, exchange_rate_from_db, base_fee, priority_fee) do %{ - "slow" => slow_fee, - "average" => average_fee, - "fast" => fast_fee + price: fee |> format_wei(), + time: time && time |> Decimal.to_float(), + fiat_price: fiat_fee(fee, exchange_rate_from_db), + base_fee: base_fee |> format_wei(), + priority_fee: base_fee && priority_fee && priority_fee |> Decimal.new() |> Wei.from(:wei) |> format_wei(), + priority_fee_wei: base_fee && priority_fee && priority_fee |> Decimal.new() |> Decimal.round(), + wei: fee |> Wei.to(:wei) |> Decimal.round() } end + defp fiat_fee(fee, exchange_rate) do + exchange_rate.usd_value && + fee + |> Wei.to(:ether) + |> Decimal.mult(exchange_rate.usd_value) + |> Decimal.mult(simple_transaction_gas()) + |> Decimal.round(2) + end + defp priority_with_base_fee(priority, base_fee) do - priority |> Wei.from(:wei) |> Wei.sum(base_fee) |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) + priority |> Wei.from(:wei) |> Wei.sum(base_fee) end defp gas_price(value) do - value |> Wei.from(:wei) |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) + value |> Wei.from(:wei) end + defp format_wei(nil), do: nil + + defp format_wei(wei), do: wei |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) + + defp global_ttl, do: Application.get_env(:explorer, __MODULE__)[:global_ttl] + + defp simple_transaction_gas, do: Application.get_env(:explorer, __MODULE__)[:simple_transaction_gas] + defp num_of_blocks, do: Application.get_env(:explorer, __MODULE__)[:num_of_blocks] defp safelow, do: Application.get_env(:explorer, __MODULE__)[:safelow_percentile] @@ -167,6 +296,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do defp fast, do: Application.get_env(:explorer, __MODULE__)[:fast_percentile] + defp safelow_time_coefficient, do: Application.get_env(:explorer, __MODULE__)[:safelow_time_coefficient] + + defp average_time_coefficient, do: Application.get_env(:explorer, __MODULE__)[:average_time_coefficient] + + defp fast_time_coefficient, do: Application.get_env(:explorer, __MODULE__)[:fast_time_coefficient] + defp handle_fallback(:gas_prices) do # This will get the task PID if one exists and launch a new task if not # See next `handle_fallback` definition @@ -181,12 +316,15 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do {:ok, task} = Task.start(fn -> try do - result = get_average_gas_price(num_of_blocks(), safelow(), average(), fast()) + {result, acc} = get_average_gas_price(num_of_blocks(), safelow(), average(), fast()) - set_gas_prices(result) + set_gas_prices_acc(acc) + set_gas_prices(%ConCache.Item{ttl: global_ttl(), value: result}) + set_old_updated_at(get_updated_at()) + set_updated_at(DateTime.utc_now()) rescue e -> - Logger.debug([ + Logger.error([ "Couldn't update gas used gas_prices", Exception.format(:error, e, __STACKTRACE__) ]) @@ -198,6 +336,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do {:update, task} end + defp handle_fallback(:gas_prices_acc) do + {:return, []} + end + defp handle_fallback(_), do: {:return, nil} # By setting this as a `callback` an async task will be started each time the diff --git a/apps/explorer/lib/explorer/chain/cache/helper.ex b/apps/explorer/lib/explorer/chain/cache/helper.ex index 61047c5f8e06..4e9bd92bd05f 100644 --- a/apps/explorer/lib/explorer/chain/cache/helper.ex +++ b/apps/explorer/lib/explorer/chain/cache/helper.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Cache.Helper do def estimated_count_from(table_name, options \\ []) do %Postgrex.Result{rows: [[count]]} = Chain.select_repo(options).query!( - "SELECT reltuples::BIGINT AS estimate FROM pg_class WHERE relname = '#{table_name}';" + "SELECT (CASE WHEN c.reltuples < 0 THEN NULL WHEN c.relpages = 0 THEN float8 '0' ELSE c.reltuples / c.relpages END * (pg_catalog.pg_relation_size(c.oid) / pg_catalog.current_setting('block_size')::int))::bigint FROM pg_catalog.pg_class c WHERE c.oid = '#{table_name}'::regclass" ) count diff --git a/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex b/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex new file mode 100644 index 000000000000..aa09cf2148ce --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex @@ -0,0 +1,54 @@ +defmodule Explorer.Chain.Cache.OptimismFinalizationPeriod do + @moduledoc """ + Caches Optimism Finalization period. + """ + + require Logger + + use Explorer.Chain.MapCache, + name: :optimism_finalization_period, + key: :period + + import EthereumJSONRPC, only: [json_rpc: 2, quantity_to_integer: 1] + + alias EthereumJSONRPC.Contract + alias Indexer.Fetcher.Optimism + alias Indexer.Fetcher.Optimism.OutputRoot + + defp handle_fallback(:period) do + optimism_l1_rpc = Application.get_all_env(:indexer)[Optimism][:optimism_l1_rpc] + output_oracle = Application.get_all_env(:indexer)[OutputRoot][:output_oracle] + + # call FINALIZATION_PERIOD_SECONDS() public getter of L2OutputOracle contract on L1 + request = Contract.eth_call_request("0xf4daa291", output_oracle, 0, nil, nil) + + case json_rpc(request, json_rpc_named_arguments(optimism_l1_rpc)) do + {:ok, value} -> + {:update, quantity_to_integer(value)} + + {:error, reason} -> + Logger.debug([ + "Couldn't fetch Optimism finalization period, reason: #{inspect(reason)}" + ]) + + {:return, nil} + end + end + + defp handle_fallback(_key), do: {:return, nil} + + defp json_rpc_named_arguments(optimism_l1_rpc) do + [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: optimism_l1_rpc, + http_options: [ + recv_timeout: :timer.minutes(10), + timeout: :timer.minutes(10), + hackney: [pool: :ethereum_jsonrpc] + ] + ] + ] + end +end diff --git a/apps/explorer/lib/explorer/chain/cache/pending_block_operation.ex b/apps/explorer/lib/explorer/chain/cache/pending_block_operation.ex index dc0f01e59cbe..6980087afa79 100644 --- a/apps/explorer/lib/explorer/chain/cache/pending_block_operation.ex +++ b/apps/explorer/lib/explorer/chain/cache/pending_block_operation.ex @@ -28,7 +28,7 @@ defmodule Explorer.Chain.Cache.PendingBlockOperation do if is_nil(cached_value) do count = Helper.estimated_count_from("pending_block_operations") - max(count, 0) + if is_nil(count), do: 0, else: max(count, 0) else cached_value end diff --git a/apps/explorer/lib/explorer/chain/cache/shibarium_counter.ex b/apps/explorer/lib/explorer/chain/cache/shibarium_counter.ex new file mode 100644 index 000000000000..6a5ed7780f25 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/shibarium_counter.ex @@ -0,0 +1,58 @@ +defmodule Explorer.Chain.Cache.ShibariumCounter do + @moduledoc """ + Caches the number of deposits and withdrawals for Shibarium Bridge. + """ + + alias Explorer.Chain + + @deposits_counter_type "shibarium_deposits_counter" + @withdrawals_counter_type "shibarium_withdrawals_counter" + + @doc """ + Fetches the cached deposits count from the `last_fetched_counters` table. + """ + def deposits_count(options \\ []) do + Chain.get_last_fetched_counter(@deposits_counter_type, options) + end + + @doc """ + Fetches the cached withdrawals count from the `last_fetched_counters` table. + """ + def withdrawals_count(options \\ []) do + Chain.get_last_fetched_counter(@withdrawals_counter_type, options) + end + + @doc """ + Stores or increments the current deposits count in the `last_fetched_counters` table. + """ + def deposits_count_save(count, just_increment \\ false) do + if just_increment do + Chain.increment_last_fetched_counter( + @deposits_counter_type, + count + ) + else + Chain.upsert_last_fetched_counter(%{ + counter_type: @deposits_counter_type, + value: count + }) + end + end + + @doc """ + Stores or increments the current withdrawals count in the `last_fetched_counters` table. + """ + def withdrawals_count_save(count, just_increment \\ false) do + if just_increment do + Chain.increment_last_fetched_counter( + @withdrawals_counter_type, + count + ) + else + Chain.upsert_last_fetched_counter(%{ + counter_type: @withdrawals_counter_type, + value: count + }) + end + end +end diff --git a/apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex b/apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex new file mode 100644 index 000000000000..1034465848c6 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/stability_validators_counters.ex @@ -0,0 +1,105 @@ +defmodule Explorer.Chain.Cache.StabilityValidatorsCounters do + @moduledoc """ + Counts and store counters of validators stability. + + It loads the count asynchronously and in a time interval of 30 minutes. + """ + + use GenServer + + alias Explorer.Chain + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + + @validators_counter_key "stability_validators_counter" + @new_validators_counter_key "new_stability_validators_counter" + @active_validators_counter_key "active_stability_validators_counter" + + # It is undesirable to automatically start the consolidation in all environments. + # Consider the test environment: if the consolidation initiates but does not + # finish before a test ends, that test will fail. This way, hundreds of + # tests were failing before disabling the consolidation and the scheduler in + # the test env. + config = Application.compile_env(:explorer, __MODULE__) + @enable_consolidation Keyword.get(config, :enable_consolidation) + + @update_interval_in_milliseconds Keyword.get(config, :update_interval_in_milliseconds) + + @doc """ + Starts a process to periodically update validators stability counters + """ + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @impl true + def init(_args) do + {:ok, %{consolidate?: @enable_consolidation}, {:continue, :ok}} + end + + defp schedule_next_consolidation do + Process.send_after(self(), :consolidate, @update_interval_in_milliseconds) + end + + @impl true + def handle_continue(:ok, %{consolidate?: true} = state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @impl true + def handle_continue(:ok, state) do + {:noreply, state} + end + + @impl true + def handle_info(:consolidate, state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @doc """ + Fetches values for a stability validators counters from the `last_fetched_counters` table. + """ + @spec get_counters(Keyword.t()) :: map() + def get_counters(options) do + %{ + validators_counter: Chain.get_last_fetched_counter(@validators_counter_key, options), + new_validators_counter: Chain.get_last_fetched_counter(@new_validators_counter_key, options), + active_validators_counter: Chain.get_last_fetched_counter(@active_validators_counter_key, options) + } + end + + @doc """ + Consolidates the info by populating the `last_fetched_counters` table with the current database information. + """ + @spec consolidate() :: any() + def consolidate do + tasks = [ + Task.async(fn -> ValidatorStability.count_validators() end), + Task.async(fn -> ValidatorStability.count_new_validators() end), + Task.async(fn -> ValidatorStability.count_active_validators() end) + ] + + [validators_counter, new_validators_counter, active_validators_counter] = Task.await_many(tasks, :infinity) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @validators_counter_key, + value: validators_counter + }) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @new_validators_counter_key, + value: new_validators_counter + }) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @active_validators_counter_key, + value: active_validators_counter + }) + end +end diff --git a/apps/explorer/lib/explorer/chain/cache/transaction.ex b/apps/explorer/lib/explorer/chain/cache/transaction.ex index 299294b1534c..dd909502a4d3 100644 --- a/apps/explorer/lib/explorer/chain/cache/transaction.ex +++ b/apps/explorer/lib/explorer/chain/cache/transaction.ex @@ -29,7 +29,7 @@ defmodule Explorer.Chain.Cache.Transaction do if is_nil(cached_value) do count = Helper.estimated_count_from("transactions") - max(count, 0) + if is_nil(count), do: 0, else: count else cached_value end diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 05a82406c95d..e23c7811f1aa 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -5,18 +5,13 @@ defmodule Explorer.Chain.ContractMethod do require Logger + import Ecto.Query, only: [from: 2] use Explorer.Schema alias Explorer.Chain.{Hash, MethodIdentifier, SmartContract} alias Explorer.Repo - @type t :: %__MODULE__{ - identifier: MethodIdentifier.t(), - abi: map(), - type: String.t() - } - - schema "contract_methods" do + typed_schema "contract_methods" do field(:identifier, MethodIdentifier) field(:abi, :map) field(:type, :string) @@ -69,13 +64,30 @@ defmodule Explorer.Chain.ContractMethod do end end + @doc """ + Finds limited number of contract methods by selector id + """ + @spec find_contract_method_query(binary(), integer()) :: Ecto.Query.t() + def find_contract_method_query(method_id, limit) do + from( + contract_method in __MODULE__, + where: contract_method.identifier == ^method_id, + limit: ^limit + ) + end + defp abi_element_to_contract_method(element) do case ABI.parse_specification([element], include_events?: true) do [selector] -> now = DateTime.utc_now() + # For events, the method_id (signature) is 32 bytes, whereas for methods + # and errors it is 4 bytes. To avoid complications with different sizes, + # we always take only the first 4 bytes of the hash. + <> = selector.method_id + %{ - identifier: selector.method_id, + identifier: first_four_bytes, abi: element, type: Atom.to_string(selector.type), inserted_at: now, diff --git a/apps/explorer/lib/explorer/chain/csv_export/address_internal_transaction_csv_exporter.ex b/apps/explorer/lib/explorer/chain/csv_export/address_internal_transaction_csv_exporter.ex index f54ae2b36d13..de4bcad24d12 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address_internal_transaction_csv_exporter.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address_internal_transaction_csv_exporter.ex @@ -34,7 +34,7 @@ defmodule Explorer.Chain.CSVExport.AddressInternalTransactionCsvExporter do |> Keyword.put(:paging_options, paging_options) |> Keyword.put(:from_block, from_block) |> Keyword.put(:to_block, to_block) - |> (&if(Helper.is_valid_filter?(filter_type, filter_value, "internal_transactions"), + |> (&if(Helper.valid_filter?(filter_type, filter_value, "internal_transactions"), do: &1 |> Keyword.put(:direction, String.to_atom(filter_value)), else: &1 )).() diff --git a/apps/explorer/lib/explorer/chain/csv_export/address_token_transfer_csv_exporter.ex b/apps/explorer/lib/explorer/chain/csv_export/address_token_transfer_csv_exporter.ex index 704229757280..93ff4305113d 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address_token_transfer_csv_exporter.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address_token_transfer_csv_exporter.ex @@ -11,8 +11,8 @@ defmodule Explorer.Chain.CSVExport.AddressTokenTransferCsvExporter do where: 3 ] - alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.{Address, Hash, TokenTransfer} + alias Explorer.{PagingOptions, Repo} + alias Explorer.Chain.{Address, DenormalizationHelper, Hash, TokenTransfer, Transaction} alias Explorer.Chain.CSVExport.Helper @paging_options %PagingOptions{page_size: Helper.limit(), asc_order: true} @@ -68,7 +68,7 @@ defmodule Explorer.Chain.CSVExport.AddressTokenTransferCsvExporter do [ to_string(token_transfer.transaction_hash), token_transfer.transaction.block_number, - token_transfer.transaction.block.timestamp, + Transaction.block_timestamp(token_transfer.transaction), Address.checksum(token_transfer.from_address_hash), Address.checksum(token_transfer.to_address_hash), Address.checksum(token_transfer.token_contract_address_hash), @@ -92,7 +92,7 @@ defmodule Explorer.Chain.CSVExport.AddressTokenTransferCsvExporter do defp fee(transaction) do transaction - |> Chain.fee(:wei) + |> Transaction.fee(:wei) |> case do {:actual, value} -> value {:maximum, value} -> "Max of #{value}" @@ -118,7 +118,7 @@ defmodule Explorer.Chain.CSVExport.AddressTokenTransferCsvExporter do query |> handle_token_transfer_paging_options(paging_options) - |> preload(transaction: :block) + |> preload(^DenormalizationHelper.extend_transaction_preload([:transaction])) |> preload(:token) |> Repo.all() end diff --git a/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex b/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex index cd2595a7e3a7..f7263c89c0e7 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex @@ -8,20 +8,14 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do from: 2 ] - alias Explorer.{Chain, Market, PagingOptions, Repo} + alias Explorer.{Market, PagingOptions, Repo} alias Explorer.Market.MarketHistory - alias Explorer.Chain.{Address, Transaction, Wei} + alias Explorer.Chain.{Address, DenormalizationHelper, Hash, Transaction, Wei} alias Explorer.Chain.CSVExport.Helper - @necessity_by_association [ - necessity_by_association: %{ - :block => :required - } - ] - @paging_options %PagingOptions{page_size: Helper.limit()} - @spec export(Address.t(), String.t(), String.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() + @spec export(Hash.Address.t(), String.t(), String.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() def export(address_hash, from_period, to_period, filter_type \\ nil, filter_value \\ nil) do {from_block, to_block} = Helper.block_from_period(from_period, to_period) exchange_rate = Market.get_coin_exchange_rate() @@ -35,16 +29,17 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do # sobelow_skip ["DOS.StringToAtom"] def fetch_transactions(address_hash, from_block, to_block, filter_type, filter_value, paging_options) do options = - @necessity_by_association + [] + |> DenormalizationHelper.extend_block_necessity(:required) |> Keyword.put(:paging_options, paging_options) |> Keyword.put(:from_block, from_block) |> Keyword.put(:to_block, to_block) - |> (&if(Helper.is_valid_filter?(filter_type, filter_value, "transactions"), + |> (&if(Helper.valid_filter?(filter_type, filter_value, "transactions"), do: &1 |> Keyword.put(:direction, String.to_atom(filter_value)), else: &1 )).() - Chain.address_to_transactions_without_rewards(address_hash, options) + Transaction.address_to_transactions_without_rewards(address_hash, options) end defp to_csv_format(transactions, address_hash, exchange_rate) do @@ -67,7 +62,7 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do date_to_prices = Enum.reduce(transactions, %{}, fn tx, acc -> - date = DateTime.to_date(tx.block.timestamp) + date = tx |> Transaction.block_timestamp() |> DateTime.to_date() if Map.has_key?(acc, date) do acc @@ -79,12 +74,12 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do transaction_lists = transactions |> Stream.map(fn transaction -> - {opening_price, closing_price} = date_to_prices[DateTime.to_date(transaction.block.timestamp)] + {opening_price, closing_price} = date_to_prices[DateTime.to_date(Transaction.block_timestamp(transaction))] [ to_string(transaction.hash), transaction.block_number, - transaction.block.timestamp, + Transaction.block_timestamp(transaction), Address.checksum(transaction.from_address_hash), Address.checksum(transaction.to_address_hash), Address.checksum(transaction.created_contract_address_hash), @@ -110,7 +105,7 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do defp fee(transaction) do transaction - |> Chain.fee(:wei) + |> Transaction.fee(:wei) |> case do {:actual, value} -> value {:maximum, value} -> "Max of #{value}" diff --git a/apps/explorer/lib/explorer/chain/csv_export/helper.ex b/apps/explorer/lib/explorer/chain/csv_export/helper.ex index 92cf9687c427..ea9e6c9fc129 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/helper.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/helper.ex @@ -88,16 +88,16 @@ defmodule Explorer.Chain.CSVExport.Helper do ["to", "from"] end - @spec is_valid_filter?(String.t(), String.t(), String.t()) :: boolean() - def is_valid_filter?(filter_type, filter_value, item_type) do - is_valid_filter_type(filter_type, filter_value, item_type) && is_valid_filter_value(filter_type, filter_value) + @spec valid_filter?(String.t(), String.t(), String.t()) :: boolean() + def valid_filter?(filter_type, filter_value, item_type) do + valid_filter_type?(filter_type, filter_value, item_type) && valid_filter_value?(filter_type, filter_value) end - defp is_valid_filter_type(filter_type, filter_value, item_type) do + defp valid_filter_type?(filter_type, filter_value, item_type) do filter_type in supported_filters(item_type) && filter_value && filter_value !== "" end - defp is_valid_filter_value(filter_type, filter_value) do + defp valid_filter_value?(filter_type, filter_value) do case filter_type do "address" -> filter_value in supported_address_filter_values() diff --git a/apps/explorer/lib/explorer/chain/decompiled_smart_contract.ex b/apps/explorer/lib/explorer/chain/decompiled_smart_contract.ex index 795214604482..31e92d38bacc 100644 --- a/apps/explorer/lib/explorer/chain/decompiled_smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/decompiled_smart_contract.ex @@ -9,16 +9,17 @@ defmodule Explorer.Chain.DecompiledSmartContract do @derive {Jason.Encoder, only: [:address_hash, :decompiler_version, :decompiled_source_code]} - schema "decompiled_smart_contracts" do - field(:decompiler_version, :string) - field(:decompiled_source_code, :string) + typed_schema "decompiled_smart_contracts" do + field(:decompiler_version, :string, null: false) + field(:decompiled_source_code, :string, null: false) belongs_to( :address, Address, foreign_key: :address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false ) timestamps() diff --git a/apps/explorer/lib/explorer/chain/denormalization_helper.ex b/apps/explorer/lib/explorer/chain/denormalization_helper.ex new file mode 100644 index 000000000000..0839080ad23a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/denormalization_helper.ex @@ -0,0 +1,52 @@ +defmodule Explorer.Chain.DenormalizationHelper do + @moduledoc """ + Helper functions for dynamic logic based on denormalization migration completeness + """ + + alias Explorer.Chain.Cache.BackgroundMigrations + + @spec extend_block_necessity(keyword(), :optional | :required) :: keyword() + def extend_block_necessity(opts, necessity \\ :optional) do + if transactions_denormalization_finished?() do + opts + else + Keyword.update(opts, :necessity_by_association, %{:block => necessity}, &Map.put(&1, :block, necessity)) + end + end + + @spec extend_transaction_block_necessity(keyword(), :optional | :required) :: keyword() + def extend_transaction_block_necessity(opts, necessity \\ :optional) do + if transactions_denormalization_finished?() do + opts + else + Keyword.update( + opts, + :necessity_by_association, + %{[transaction: :block] => necessity}, + &(&1 |> Map.delete(:transaction) |> Map.put([transaction: :block], necessity)) + ) + end + end + + @spec extend_transaction_preload(list()) :: list() + def extend_transaction_preload(preloads) do + if transactions_denormalization_finished?() do + preloads + else + [transaction: :block] ++ (preloads -- [:transaction]) + end + end + + @spec extend_block_preload(list()) :: list() + def extend_block_preload(preloads) do + if transactions_denormalization_finished?() do + preloads + else + [:block | preloads] + end + end + + def transactions_denormalization_finished?, do: BackgroundMigrations.get_transactions_denormalization_finished() + + def tt_denormalization_finished?, do: BackgroundMigrations.get_tt_denormalization_finished() +end diff --git a/apps/explorer/lib/explorer/chain/events/publisher.ex b/apps/explorer/lib/explorer/chain/events/publisher.ex index 21b8d168af92..d6e4aa52b766 100644 --- a/apps/explorer/lib/explorer/chain/events/publisher.ex +++ b/apps/explorer/lib/explorer/chain/events/publisher.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Publisher do Publishes events related to the Chain context. """ - @allowed_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number polygon_edge_reorg_block token_transfers transactions contract_verification_result token_total_supply changed_bytecode smart_contract_was_verified zkevm_confirmed_batches)a + @allowed_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number optimism_deposits token_transfers transactions contract_verification_result token_total_supply changed_bytecode smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a def broadcast(_data, false), do: :ok diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex index 9d049758ec56..0a285f73f596 100644 --- a/apps/explorer/lib/explorer/chain/events/subscriber.ex +++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Subscriber do Subscribes to events related to the Chain context. """ - @allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number polygon_edge_reorg_block token_transfers transactions contract_verification_result token_total_supply changed_bytecode smart_contract_was_verified zkevm_confirmed_batches)a + @allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number optimism_deposits token_transfers transactions contract_verification_result token_total_supply changed_bytecode smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a @allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a diff --git a/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex b/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex index 1e6bd9e6eb68..47d4cb592098 100644 --- a/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex +++ b/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex @@ -34,6 +34,8 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do end defp fetch_sources(address, only_full?) do + Publisher.broadcast(%{eth_bytecode_db_lookup_started: [address.hash]}, :on_demand) + creation_tx_input = contract_creation_input(address.hash) with {:ok, %{"sourceType" => type, "matchType" => match_type} = source} <- @@ -45,6 +47,7 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do Publisher.broadcast(%{smart_contract_was_verified: [address.hash]}, :on_demand) else _ -> + Publisher.broadcast(%{smart_contract_was_not_verified: [address.hash]}, :on_demand) false end end @@ -127,15 +130,15 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do end def process_contract_source("SOLIDITY", source, address_hash) do - SolidityPublisher.process_rust_verifier_response(source, address_hash, true, true, true) + SolidityPublisher.process_rust_verifier_response(source, address_hash, %{}, true, true, true) end def process_contract_source("VYPER", source, address_hash) do - VyperPublisher.process_rust_verifier_response(source, address_hash, true, true, true) + VyperPublisher.process_rust_verifier_response(source, address_hash, %{}, true, true, true) end def process_contract_source("YUL", source, address_hash) do - SolidityPublisher.process_rust_verifier_response(source, address_hash, true, true, true) + SolidityPublisher.process_rust_verifier_response(source, address_hash, %{}, true, true, true) end def process_contract_source(_, _source, _address_hash), do: false diff --git a/apps/explorer/lib/explorer/chain/hash/address.ex b/apps/explorer/lib/explorer/chain/hash/address.ex index 930774ee5944..f37db26dee9d 100644 --- a/apps/explorer/lib/explorer/chain/hash/address.ex +++ b/apps/explorer/lib/explorer/chain/hash/address.ex @@ -169,9 +169,9 @@ defmodule Explorer.Chain.Hash.Address do @spec validate(String.t()) :: {:ok, String.t()} | {:error, :invalid_length | :invalid_characters | :invalid_checksum} def validate("0x" <> hash) do with {:length, true} <- {:length, String.length(hash) == 40}, - {:hex, true} <- {:hex, is_hex?(hash)}, - {:mixed_case, true} <- {:mixed_case, is_mixed_case?(hash)}, - {:checksummed, true} <- {:checksummed, is_checksummed?(hash)} do + {:hex, true} <- {:hex, hex?(hash)}, + {:mixed_case, true} <- {:mixed_case, mixed_case?(hash)}, + {:checksummed, true} <- {:checksummed, checksummed?(hash)} do {:ok, "0x" <> hash} else {:length, false} -> @@ -188,16 +188,16 @@ defmodule Explorer.Chain.Hash.Address do end end - @spec is_hex?(String.t()) :: boolean() - defp is_hex?(hash) do + @spec hex?(String.t()) :: boolean() + defp hex?(hash) do case Regex.run(~r|[0-9a-f]{40}|i, hash) do nil -> false [_] -> true end end - @spec is_mixed_case?(String.t()) :: boolean() - defp is_mixed_case?(hash) do + @spec mixed_case?(String.t()) :: boolean() + defp mixed_case?(hash) do upper_check = ~r|[0-9A-F]{40}| lower_check = ~r|[0-9a-f]{40}| @@ -209,8 +209,8 @@ defmodule Explorer.Chain.Hash.Address do end end - @spec is_checksummed?(String.t()) :: boolean() - defp is_checksummed?(original_hash) do + @spec checksummed?(String.t()) :: boolean() + defp checksummed?(original_hash) do lowercase_hash = String.downcase(original_hash) sha3_hash = ExKeccak.hash_256(lowercase_hash) @@ -224,15 +224,15 @@ defmodule Explorer.Chain.Hash.Address do <> = sha3_hash <> = address_hash - if is_proper_case?(checksum_digit, current_char) do + if proper_case?(checksum_digit, current_char) do do_checksum_check(remaining_sha3_hash, remaining_address_hash) else false end end - @spec is_proper_case?(integer, String.t()) :: boolean() - defp is_proper_case?(checksum_digit, character) do + @spec proper_case?(integer, String.t()) :: boolean() + defp proper_case?(checksum_digit, character) do case_map = %{ "0" => :both, "1" => :both, diff --git a/apps/explorer/lib/explorer/chain/hash/full.ex b/apps/explorer/lib/explorer/chain/hash/full.ex index eb8ae148dd3a..db2a586bd770 100644 --- a/apps/explorer/lib/explorer/chain/hash/full.ex +++ b/apps/explorer/lib/explorer/chain/hash/full.ex @@ -90,7 +90,7 @@ defmodule Explorer.Chain.Hash.Full do ...> ) {:ok, <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>>} - If the field from the struct is an incorrect format such as `t:Explorer.Chain.Address.Hash.t/0`, `:error` is returned. + If the field from the struct is an incorrect format such as `t:Explorer.Chain.Hash.Address.t/0`, `:error` is returned. iex> Explorer.Chain.Hash.Full.dump( ...> %Explorer.Chain.Hash{ diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 138db2c676fa..24f017c39228 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -12,15 +12,15 @@ defmodule Explorer.Chain.Import do require Logger @stages [ - Import.Stage.Addresses, - Import.Stage.AddressReferencing, + Import.Stage.AddressesBlocksCoinBalances, Import.Stage.BlockReferencing, Import.Stage.BlockFollowing, Import.Stage.BlockPending ] # in order so that foreign keys are inserted before being referenced - @runners Enum.flat_map(@stages, fn stage -> stage.runners() end) + @configured_runners Enum.flat_map(@stages, fn stage -> stage.runners() end) + @all_runners Enum.flat_map(@stages, fn stage -> stage.all_runners() end) quoted_runner_option_value = quote do @@ -28,7 +28,7 @@ defmodule Explorer.Chain.Import do end quoted_runner_options = - for runner <- @runners do + for runner <- @all_runners do quoted_key = quote do optional(unquote(runner.option_key())) @@ -44,7 +44,7 @@ defmodule Explorer.Chain.Import do } quoted_runner_imported = - for runner <- @runners do + for runner <- @all_runners do quoted_key = quote do optional(unquote(runner.option_key())) @@ -69,7 +69,7 @@ defmodule Explorer.Chain.Import do # milliseconds @transaction_timeout :timer.minutes(4) - @imported_table_rows @runners + @imported_table_rows @all_runners |> Stream.map(&Map.put(&1.imported_table_row(), :key, &1.option_key())) |> Enum.map_join("\n", fn %{ key: key, @@ -78,7 +78,7 @@ defmodule Explorer.Chain.Import do } -> "| `#{inspect(key)}` | `#{value_type}` | #{value_description} |" end) - @runner_options_doc Enum.map_join(@runners, fn runner -> + @runner_options_doc Enum.map_join(@all_runners, fn runner -> ecto_schema_module = runner.ecto_schema_module() """ @@ -123,7 +123,7 @@ defmodule Explorer.Chain.Import do milliseconds. #{@runner_options_doc} """ - @spec all(all_options()) :: all_result() + # @spec all(all_options()) :: all_result() def all(options) when is_map(options) do with {:ok, runner_options_pairs} <- validate_options(options), {:ok, valid_runner_option_pairs} <- validate_runner_options_pairs(runner_options_pairs), @@ -188,7 +188,8 @@ defmodule Explorer.Chain.Import do local_options = Map.drop(options, @global_options) {reverse_runner_options_pairs, unknown_options} = - Enum.reduce(@runners, {[], local_options}, fn runner, {acc_runner_options_pairs, unknown_options} = acc -> + Enum.reduce(@configured_runners, {[], local_options}, fn runner, + {acc_runner_options_pairs, unknown_options} = acc -> option_key = runner.option_key() case local_options do diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex index cc51fb87376c..b7420aec8b24 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex @@ -219,7 +219,8 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do ordered_changes_list = changes_list |> Enum.map(fn change -> - if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do + if Map.has_key?(change, :token_id) and + (Map.get(change, :token_type) == "ERC-1155" || Map.get(change, :token_type) == "ERC-404") do change else Map.put(change, :token_id, nil) diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex index 618c8a920de2..16989c2822f6 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex @@ -69,10 +69,11 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do ordered_changes_list = changes_list |> Enum.map(fn change -> - if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do - change - else - Map.put(change, :token_id, nil) + cond do + Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" -> change + Map.get(change, :token_type) == "ERC-404" and Map.has_key?(change, :token_id) -> Map.put(change, :value, nil) + Map.get(change, :token_type) == "ERC-404" and Map.has_key?(change, :value) -> Map.put(change, :token_id, nil) + true -> Map.put(change, :token_id, nil) end end) |> Enum.group_by(fn %{ diff --git a/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex new file mode 100644 index 000000000000..e3f61616cd19 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex @@ -0,0 +1,115 @@ +defmodule Explorer.Chain.Import.Runner.Beacon.BlobTransactions do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Beacon.BlobTransaction.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Explorer.Chain.Beacon.BlobTransaction + alias Ecto.{Multi, Repo} + alias Explorer.Chain.{Hash, Import} + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Hash.Full.t()] + + @impl Import.Runner + def ecto_schema_module, do: BlobTransaction + + @impl Import.Runner + def option_key, do: :beacon_blob_transactions + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + # Enforce ShareLocks tables order (see docs: sharelocks.md) + multi + |> Multi.run(:beacon_blob_transactions, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :beacon_blob_transactions, + :beacon_blob_transactions + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: {:ok, [Hash.t()]} + defp insert( + repo, + changes_list, + %{ + timeout: timeout, + timestamps: timestamps + } = options + ) + when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce Transaction ShareLocks order (see docs: sharelocks.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.hash) + + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: :hash, + on_conflict: on_conflict, + for: BlobTransaction, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + end + + defp default_on_conflict do + from( + blob_transaction in BlobTransaction, + update: [ + set: [ + max_fee_per_blob_gas: fragment("EXCLUDED.max_fee_per_blob_gas"), + blob_versioned_hashes: fragment("EXCLUDED.blob_versioned_hashes"), + blob_gas_used: fragment("EXCLUDED.blob_gas_used"), + blob_gas_price: fragment("EXCLUDED.blob_gas_price"), + # Don't update `hash` as it is part of the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", blob_transaction.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", blob_transaction.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.max_fee_per_blob_gas, EXCLUDED.blob_versioned_hashes, EXCLUDED.blob_gas_used, EXCLUDED.blob_gas_price) IS DISTINCT FROM (?, ?, ?, ?)", + blob_transaction.max_fee_per_blob_gas, + blob_transaction.blob_versioned_hashes, + blob_transaction.blob_gas_used, + blob_transaction.blob_gas_price + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index defc998b866d..293a746b5f45 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -9,9 +9,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Ecto.{Changeset, Multi, Repo} + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.Chain.{ Address, Block, + BlockNumberHelper, + DenormalizationHelper, Import, PendingBlockOperation, Token, @@ -60,19 +64,14 @@ defmodule Explorer.Chain.Import.Runner.Blocks do hashes = Enum.map(changes_list, & &1.hash) - items_for_pending_ops = - changes_list - |> filter_by_height_range(&is_block_in_range?(&1.number)) - |> Enum.filter(& &1.consensus) - |> Enum.map(&{&1.number, &1.hash}) - consensus_block_numbers = consensus_block_numbers(changes_list) # Enforce ShareLocks tables order (see docs: sharelocks.md) run_func = fn repo -> {:ok, nonconsensus_items} = lose_consensus(repo, hashes, consensus_block_numbers, changes_list, insert_options) - {:ok, filter_by_height_range(nonconsensus_items, fn {number, _hash} -> is_block_in_range?(number) end)} + {:ok, + filter_by_height_range(nonconsensus_items, fn {number, _hash} -> RangesHelper.traceable_block_number?(number) end)} end multi @@ -95,10 +94,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do :blocks ) end) - |> Multi.run(:new_pending_operations, fn repo, %{lose_consensus: nonconsensus_items} -> + |> Multi.run(:new_pending_operations, fn repo, %{blocks: blocks} -> Instrumenter.block_import_stage_runner( fn -> - new_pending_operations(repo, nonconsensus_items, items_for_pending_ops, insert_options) + new_pending_operations(repo, blocks, insert_options) end, :address_referencing, :blocks, @@ -214,12 +213,6 @@ defmodule Explorer.Chain.Import.Runner.Blocks do @impl Runner def timeout, do: @timeout - defp is_block_in_range?(number) do - minimal_block_height = Application.get_env(:indexer, :trace_first_block) - maximal_block_height = Application.get_env(:indexer, :trace_last_block) - number >= minimal_block_height && if(maximal_block_height, do: number <= maximal_block_height, else: true) - end - defp fork_transactions(%{ repo: repo, timeout: timeout, @@ -378,7 +371,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do or_where: block.number in ^consensus_block_numbers, # we also need to acquire blocks that will be upserted here, for ordering or_where: block.hash in ^hashes, - select: block.hash, + select: %{hash: block.hash, number: block.number}, # Enforce Block ShareLocks order (see docs: sharelocks.md) order_by: [asc: block.hash], lock: "FOR NO KEY UPDATE" @@ -398,6 +391,30 @@ defmodule Explorer.Chain.Import.Runner.Blocks do timeout: timeout ) + repo.update_all( + from( + transaction in Transaction, + join: s in subquery(acquire_query), + on: transaction.block_hash == s.hash, + # we don't want to remove consensus from blocks that will be upserted + where: transaction.block_hash not in ^hashes + ), + [set: [block_consensus: false, updated_at: updated_at]], + timeout: timeout + ) + + repo.update_all( + from( + token_transfer in TokenTransfer, + join: s in subquery(acquire_query), + on: token_transfer.block_number == s.number, + # we don't want to remove consensus from blocks that will be upserted + where: token_transfer.block_hash not in ^hashes + ), + [set: [block_consensus: false, updated_at: updated_at]], + timeout: timeout + ) + removed_consensus_block_hashes |> Enum.map(fn {number, _hash} -> number end) |> Enum.reject(&Enum.member?(consensus_block_numbers, &1)) @@ -418,18 +435,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do lose_consensus(ExplorerRepo, [], block_numbers, [], opts) end - defp new_pending_operations(repo, nonconsensus_items, items, %{ - timeout: timeout, - timestamps: timestamps - }) do + defp new_pending_operations(repo, inserted_blocks, %{timeout: timeout, timestamps: timestamps}) do sorted_pending_ops = - items - |> MapSet.new() - |> MapSet.difference(MapSet.new(nonconsensus_items)) + inserted_blocks + |> filter_by_height_range(&RangesHelper.traceable_block_number?(&1.number)) + |> Enum.filter(& &1.consensus) + |> Enum.map(&%{block_hash: &1.hash, block_number: &1.number}) |> Enum.sort() - |> Enum.map(fn {number, hash} -> - %{block_hash: hash, block_number: number} - end) Import.insert_changes_list( repo, @@ -509,8 +521,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do select: map(ctb, [ :address_hash, + :block_number, :token_contract_address_hash, :token_id, + :token_type, # Used to determine if `address_hash` was a holder of `token_contract_address_hash` before # `address_current_token_balance` is deleted in `update_tokens_holder_count`. @@ -543,43 +557,28 @@ defmodule Explorer.Chain.Import.Runner.Blocks do %{timeout: timeout} = options ) when is_list(deleted_address_current_token_balances) do - final_query = derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances) - - new_current_token_balance_query = - from(new_current_token_balance in subquery(final_query), - inner_join: tb in Address.TokenBalance, - on: - tb.address_hash == new_current_token_balance.address_hash and - tb.token_contract_address_hash == new_current_token_balance.token_contract_address_hash and - ((is_nil(tb.token_id) and is_nil(new_current_token_balance.token_id)) or - (tb.token_id == new_current_token_balance.token_id and - not is_nil(tb.token_id) and not is_nil(new_current_token_balance.token_id))) and - tb.block_number == new_current_token_balance.block_number, - select: %{ - address_hash: new_current_token_balance.address_hash, - token_contract_address_hash: new_current_token_balance.token_contract_address_hash, - token_id: new_current_token_balance.token_id, - token_type: tb.token_type, - block_number: new_current_token_balance.block_number, - value: tb.value, - value_fetched_at: tb.value_fetched_at, - inserted_at: over(min(tb.inserted_at), :w), - updated_at: over(max(tb.updated_at), :w) - }, - windows: [ - w: [partition_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id]] - ] - ) + new_current_token_balances_placeholders = + Enum.map(deleted_address_current_token_balances, fn deleted_balance -> + now = DateTime.utc_now() - current_token_balance = - new_current_token_balance_query - |> repo.all() + %{ + address_hash: deleted_balance.address_hash, + token_contract_address_hash: deleted_balance.token_contract_address_hash, + token_id: deleted_balance.token_id, + token_type: deleted_balance.token_type, + block_number: deleted_balance.block_number, + value: nil, + value_fetched_at: nil, + inserted_at: now, + updated_at: now + } + end) timestamps = Import.timestamps() result = CurrentTokenBalances.insert_changes_list_with_and_without_token_id( - current_token_balance, + new_current_token_balances_placeholders, repo, timestamps, timeout, @@ -700,28 +699,51 @@ defmodule Explorer.Chain.Import.Runner.Blocks do end defp forked_token_transfers_query(forked_transaction_hashes) do - from(token_transfer in TokenTransfer, - where: token_transfer.transaction_hash in ^forked_transaction_hashes, - inner_join: token in Token, - on: token.contract_address_hash == token_transfer.token_contract_address_hash, - where: token.type == "ERC-721", - inner_join: instance in Instance, - on: - fragment("? @> ARRAY[?::decimal]", token_transfer.token_ids, instance.token_id) and - instance.token_contract_address_hash == token_transfer.token_contract_address_hash, - # per one token instance we will have only one token transfer - where: - token_transfer.block_number == instance.owner_updated_at_block and - token_transfer.log_index == instance.owner_updated_at_log_index, - select: %{ - from: token_transfer.from_address_hash, - to: token_transfer.to_address_hash, - token_id: instance.token_id, - token_contract_address_hash: token_transfer.token_contract_address_hash, - block_number: token_transfer.block_number, - log_index: token_transfer.log_index - } - ) + if DenormalizationHelper.tt_denormalization_finished?() do + from(token_transfer in TokenTransfer, + where: token_transfer.transaction_hash in ^forked_transaction_hashes, + where: token_transfer.token_type == "ERC-721", + inner_join: instance in Instance, + on: + fragment("? @> ARRAY[?::decimal]", token_transfer.token_ids, instance.token_id) and + instance.token_contract_address_hash == token_transfer.token_contract_address_hash, + # per one token instance we will have only one token transfer + where: + token_transfer.block_number == instance.owner_updated_at_block and + token_transfer.log_index == instance.owner_updated_at_log_index, + select: %{ + from: token_transfer.from_address_hash, + to: token_transfer.to_address_hash, + token_id: instance.token_id, + token_contract_address_hash: token_transfer.token_contract_address_hash, + block_number: token_transfer.block_number, + log_index: token_transfer.log_index + } + ) + else + from(token_transfer in TokenTransfer, + where: token_transfer.transaction_hash in ^forked_transaction_hashes, + inner_join: token in Token, + on: token.contract_address_hash == token_transfer.token_contract_address_hash, + where: token.type == "ERC-721", + inner_join: instance in Instance, + on: + fragment("? @> ARRAY[?::decimal]", token_transfer.token_ids, instance.token_id) and + instance.token_contract_address_hash == token_transfer.token_contract_address_hash, + # per one token instance we will have only one token transfer + where: + token_transfer.block_number == instance.owner_updated_at_block and + token_transfer.log_index == instance.owner_updated_at_log_index, + select: %{ + from: token_transfer.from_address_hash, + to: token_transfer.to_address_hash, + token_id: instance.token_id, + token_contract_address_hash: token_transfer.token_contract_address_hash, + block_number: token_transfer.block_number, + log_index: token_transfer.log_index + } + ) + end end defp token_instances_on_conflict do @@ -741,43 +763,6 @@ defmodule Explorer.Chain.Import.Runner.Blocks do ) end - defp derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances) do - initial_query = - from(tb in Address.TokenBalance, - select: %{ - address_hash: tb.address_hash, - token_contract_address_hash: tb.token_contract_address_hash, - token_id: tb.token_id, - block_number: max(tb.block_number) - }, - group_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id] - ) - - Enum.reduce(deleted_address_current_token_balances, initial_query, fn %{ - address_hash: address_hash, - token_contract_address_hash: - token_contract_address_hash, - token_id: token_id - }, - acc_query -> - if token_id do - from(tb in acc_query, - or_where: - tb.address_hash == ^address_hash and - tb.token_contract_address_hash == ^token_contract_address_hash and - tb.token_id == ^token_id - ) - else - from(tb in acc_query, - or_where: - tb.address_hash == ^address_hash and - tb.token_contract_address_hash == ^token_contract_address_hash and - is_nil(tb.token_id) - ) - end - end) - end - # `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is # inserted, the old block rewards need to be deleted, so that the old and new rewards aren't combined. defp delete_rewards(repo, blocks_changes, %{timeout: timeout}) do @@ -875,11 +860,14 @@ defmodule Explorer.Chain.Import.Runner.Blocks do number: number }, acc -> + previous_block_number = BlockNumberHelper.previous_block_number(number) + next_block_number = BlockNumberHelper.next_block_number(number) + if consensus do from( block in acc, - or_where: block.number == ^(number - 1) and block.hash != ^parent_hash, - or_where: block.number == ^(number + 1) and block.parent_hash != ^hash + or_where: block.number == ^previous_block_number and block.hash != ^parent_hash, + or_where: block.number == ^next_block_number and block.parent_hash != ^hash ) else acc @@ -890,10 +878,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do end defp filter_by_height_range(blocks, filter_func) do - minimal_block_height = Application.get_env(:indexer, :trace_first_block) - maximal_block_height = Application.get_env(:indexer, :trace_last_block) - - if minimal_block_height > 0 || maximal_block_height do + if RangesHelper.trace_ranges_present?() do Enum.filter(blocks, &filter_func.(&1)) else blocks diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index 0763d86ebdf2..29ab7648bb54 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -8,14 +8,15 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do alias Ecto.Adapters.SQL alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.{Block, Hash, Import, InternalTransaction, PendingBlockOperation, Transaction} + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.Chain.{Block, Hash, Import, InternalTransaction, PendingBlockOperation, TokenTransfer, Transaction} alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Import.Runner alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator - import Ecto.Query, only: [from: 2, where: 3] + import Ecto.Query @behaviour Runner @@ -146,7 +147,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end) |> Multi.run(:remove_consensus_of_invalid_blocks, fn repo, %{invalid_block_numbers: invalid_block_numbers} -> Instrumenter.block_import_stage_runner( - fn -> remove_consensus_of_invalid_blocks(repo, invalid_block_numbers) end, + fn -> remove_consensus_of_invalid_blocks(repo, invalid_block_numbers, timestamps) end, :block_pending, :internal_transactions, :remove_consensus_of_invalid_blocks @@ -332,6 +333,9 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do # common_tuples = MapSet.intersection(required_tuples, candidate_tuples) #should be added # |> MapSet.difference(internal_transactions_tuples) should be replaced with |> MapSet.difference(common_tuples) + # Note: for zetachain, the case "# - there are no internal txs for some transactions" is removed since + # there are may be non-traceable transactions + transactions_tuples = MapSet.new(transactions, &{&1.hash, &1.block_number}) internal_transactions_tuples = MapSet.new(internal_transactions_params, &{&1.transaction_hash, &1.block_number}) @@ -339,10 +343,21 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do all_tuples = MapSet.union(transactions_tuples, internal_transactions_tuples) invalid_block_numbers = - all_tuples - |> MapSet.difference(internal_transactions_tuples) - |> MapSet.new(fn {_hash, block_number} -> block_number end) - |> MapSet.to_list() + if Application.get_env(:explorer, :chain_type) == "zetachain" do + Enum.reduce(internal_transactions_tuples, [], fn {transaction_hash, block_number}, acc -> + # credo:disable-for-next-line + case Enum.find(transactions_tuples, fn {t_hash, _block_number} -> t_hash == transaction_hash end) do + nil -> acc + {_t_hash, ^block_number} -> acc + _ -> [block_number | acc] + end + end) + else + all_tuples + |> MapSet.difference(internal_transactions_tuples) + |> MapSet.new(fn {_hash, block_number} -> block_number end) + |> MapSet.to_list() + end {:ok, invalid_block_numbers} end @@ -380,7 +395,9 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do block_hash = Map.fetch!(blocks_map, block_number) entries - |> Enum.sort_by(&{&1.transaction_hash, &1.index}) + |> Enum.sort_by( + &{(Map.has_key?(&1, :transaction_index) && &1.transaction_index) || &1.transaction_hash, &1.index} + ) |> Enum.with_index() |> Enum.map(fn {entry, index} -> entry @@ -690,26 +707,40 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end - defp remove_consensus_of_invalid_blocks(repo, invalid_block_numbers) do - minimal_block = Application.get_env(:indexer, :trace_first_block) - maximal_block = Application.get_env(:indexer, :trace_last_block) - + defp remove_consensus_of_invalid_blocks(repo, invalid_block_numbers, %{updated_at: updated_at}) do if Enum.count(invalid_block_numbers) > 0 do - update_query = + update_block_query = from( block in Block, where: block.number in ^invalid_block_numbers and block.consensus == true, - where: block.number > ^minimal_block, + where: ^traceable_blocks_dynamic_query(), select: block.hash, # ShareLocks order already enforced by `acquire_blocks` (see docs: sharelocks.md) - update: [set: [consensus: false]] + update: [set: [consensus: false, updated_at: ^updated_at]] ) - update_query = - if maximal_block, do: update_query |> where([block], block.number < ^maximal_block), else: update_query + update_transaction_query = + from( + transaction in Transaction, + where: transaction.block_number in ^invalid_block_numbers and transaction.block_consensus, + where: ^traceable_block_number_dynamic_query(), + # ShareLocks order already enforced by `acquire_blocks` (see docs: sharelocks.md) + update: [set: [block_consensus: false, updated_at: ^updated_at]] + ) + + update_token_transfers_query = + from( + token_transfer in TokenTransfer, + where: token_transfer.block_number in ^invalid_block_numbers and token_transfer.block_consensus, + where: ^traceable_block_number_dynamic_query(), + # ShareLocks order already enforced by `acquire_blocks` (see docs: sharelocks.md) + update: [set: [block_consensus: false, updated_at: ^updated_at]] + ) try do - {_num, result} = repo.update_all(update_query, []) + {_num, result} = repo.update_all(update_block_query, []) + {_num, _result} = repo.update_all(update_transaction_query, []) + {_num, _result} = repo.update_all(update_token_transfers_query, []) MissingRangesManipulator.add_ranges_by_block_numbers(invalid_block_numbers) @@ -754,4 +785,30 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do {:error, %{exception: postgrex_error, pending_hashes: valid_block_hashes}} end end + + defp traceable_blocks_dynamic_query do + if RangesHelper.trace_ranges_present?() do + block_ranges = RangesHelper.get_trace_block_ranges() + + Enum.reduce(block_ranges, dynamic([_], false), fn + _from.._to = range, acc -> dynamic([block], ^acc or block.number in ^range) + num_to_latest, acc -> dynamic([block], ^acc or block.number >= ^num_to_latest) + end) + else + dynamic([_], true) + end + end + + defp traceable_block_number_dynamic_query do + if RangesHelper.trace_ranges_present?() do + block_ranges = RangesHelper.get_trace_block_ranges() + + Enum.reduce(block_ranges, dynamic([_], false), fn + _from.._to = range, acc -> dynamic([transaction_or_tt], ^acc or transaction_or_tt.block_number in ^range) + num_to_latest, acc -> dynamic([transaction_or_tt], ^acc or transaction_or_tt.block_number >= ^num_to_latest) + end) + else + dynamic([_], true) + end + end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/logs.ex b/apps/explorer/lib/explorer/chain/import/runner/logs.ex index c90adaa04a41..7c0e591a0f92 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/logs.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/logs.ex @@ -92,7 +92,6 @@ defmodule Explorer.Chain.Import.Runner.Logs do third_topic: fragment("EXCLUDED.third_topic"), fourth_topic: fragment("EXCLUDED.fourth_topic"), # Don't update `index` as it is part of the composite primary key and used for the conflict target - type: fragment("EXCLUDED.type"), # Don't update `transaction_hash` as it is part of the composite primary key and used for the conflict target inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", log.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", log.updated_at) @@ -100,14 +99,13 @@ defmodule Explorer.Chain.Import.Runner.Logs do ], where: fragment( - "(EXCLUDED.address_hash, EXCLUDED.data, EXCLUDED.first_topic, EXCLUDED.second_topic, EXCLUDED.third_topic, EXCLUDED.fourth_topic, EXCLUDED.type) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.address_hash, EXCLUDED.data, EXCLUDED.first_topic, EXCLUDED.second_topic, EXCLUDED.third_topic, EXCLUDED.fourth_topic) IS DISTINCT FROM (?, ?, ?, ?, ?, ?)", log.address_hash, log.data, log.first_topic, log.second_topic, log.third_topic, - log.fourth_topic, - log.type + log.fourth_topic ) ) end diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/deposits.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/deposits.ex new file mode 100644 index 000000000000..0422a625f35a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/deposits.ex @@ -0,0 +1,106 @@ +defmodule Explorer.Chain.Import.Runner.Optimism.Deposits do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Deposit.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Optimism.Deposit + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Deposit.t()] + + @impl Import.Runner + def ecto_schema_module, do: Deposit + + @impl Import.Runner + def option_key, do: :optimism_deposits + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_optimism_deposits, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :optimism_deposits, + :optimism_deposits + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [Deposit.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce Deposit ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.l2_transaction_hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: Deposit, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :l2_transaction_hash, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + deposit in Deposit, + update: [ + set: [ + # don't update `l2_transaction_hash` as it is a primary key and used for the conflict target + l1_block_number: fragment("EXCLUDED.l1_block_number"), + l1_block_timestamp: fragment("EXCLUDED.l1_block_timestamp"), + l1_transaction_hash: fragment("EXCLUDED.l1_transaction_hash"), + l1_transaction_origin: fragment("EXCLUDED.l1_transaction_origin"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", deposit.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", deposit.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.l1_block_number, EXCLUDED.l1_block_timestamp, EXCLUDED.l1_transaction_hash, EXCLUDED.l1_transaction_origin) IS DISTINCT FROM (?, ?, ?, ?)", + deposit.l1_block_number, + deposit.l1_block_timestamp, + deposit.l1_transaction_hash, + deposit.l1_transaction_origin + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/frame_sequences.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/frame_sequences.ex new file mode 100644 index 000000000000..bf45f5354d71 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/frame_sequences.ex @@ -0,0 +1,102 @@ +defmodule Explorer.Chain.Import.Runner.Optimism.FrameSequences do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Optimism.FrameSequence.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Optimism.FrameSequence + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [FrameSequence.t()] + + @impl Import.Runner + def ecto_schema_module, do: FrameSequence + + @impl Import.Runner + def option_key, do: :optimism_frame_sequences + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_frame_sequences, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :optimism_frame_sequences, + :optimism_frame_sequences + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [FrameSequence.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce FrameSequence ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.id) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: FrameSequence, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :id, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + fs in FrameSequence, + update: [ + set: [ + # don't update `id` as it is a primary key and used for the conflict target + l1_transaction_hashes: fragment("EXCLUDED.l1_transaction_hashes"), + l1_timestamp: fragment("EXCLUDED.l1_timestamp"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", fs.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", fs.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.l1_transaction_hashes, EXCLUDED.l1_timestamp) IS DISTINCT FROM (?, ?)", + fs.l1_transaction_hashes, + fs.l1_timestamp + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/output_roots.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/output_roots.ex new file mode 100644 index 000000000000..d0027bc483f6 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/output_roots.ex @@ -0,0 +1,108 @@ +defmodule Explorer.Chain.Import.Runner.Optimism.OutputRoots do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Optimism.OutputRoot.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Optimism.OutputRoot + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [OutputRoot.t()] + + @impl Import.Runner + def ecto_schema_module, do: OutputRoot + + @impl Import.Runner + def option_key, do: :optimism_output_roots + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_output_roots, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :optimism_output_roots, + :optimism_output_roots + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [OutputRoot.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce OutputRoot ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.l2_output_index) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: OutputRoot, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :l2_output_index, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + root in OutputRoot, + update: [ + set: [ + # don't update `l2_output_index` as it is a primary key and used for the conflict target + l2_block_number: fragment("EXCLUDED.l2_block_number"), + l1_transaction_hash: fragment("EXCLUDED.l1_transaction_hash"), + l1_timestamp: fragment("EXCLUDED.l1_timestamp"), + l1_block_number: fragment("EXCLUDED.l1_block_number"), + output_root: fragment("EXCLUDED.output_root"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", root.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", root.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.l2_block_number, EXCLUDED.l1_transaction_hash, EXCLUDED.l1_timestamp, EXCLUDED.l1_block_number, EXCLUDED.output_root) IS DISTINCT FROM (?, ?, ?, ?, ?)", + root.l2_block_number, + root.l1_transaction_hash, + root.l1_timestamp, + root.l1_block_number, + root.output_root + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/txn_batches.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/txn_batches.ex new file mode 100644 index 000000000000..5b84ef3755e3 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/txn_batches.ex @@ -0,0 +1,100 @@ +defmodule Explorer.Chain.Import.Runner.Optimism.TxnBatches do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Optimism.TxnBatch.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Optimism.TxnBatch + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [TxnBatch.t()] + + @impl Import.Runner + def ecto_schema_module, do: TxnBatch + + @impl Import.Runner + def option_key, do: :optimism_txn_batches + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_txn_batches, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :optimism_txn_batches, + :optimism_txn_batches + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [TxnBatch.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce TxnBatch ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.l2_block_number) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: TxnBatch, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :l2_block_number, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + tb in TxnBatch, + update: [ + set: [ + # don't update `l2_block_number` as it is a primary key and used for the conflict target + frame_sequence_id: fragment("EXCLUDED.frame_sequence_id"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tb.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tb.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.frame_sequence_id) IS DISTINCT FROM (?)", + tb.frame_sequence_id + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex new file mode 100644 index 000000000000..95869f543505 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex @@ -0,0 +1,105 @@ +defmodule Explorer.Chain.Import.Runner.Optimism.WithdrawalEvents do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Optimism.WithdrawalEvent.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Optimism.WithdrawalEvent + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [WithdrawalEvent.t()] + + @impl Import.Runner + def ecto_schema_module, do: WithdrawalEvent + + @impl Import.Runner + def option_key, do: :optimism_withdrawal_events + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_withdrawal_events, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :optimism_withdrawal_events, + :optimism_withdrawal_events + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [WithdrawalEvent.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce WithdrawalEvent ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.withdrawal_hash, &1.l1_event_type}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: WithdrawalEvent, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: [:withdrawal_hash, :l1_event_type], + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + we in WithdrawalEvent, + update: [ + set: [ + # don't update `withdrawal_hash` as it is a part of the composite primary key and used for the conflict target + # don't update `l1_event_type` as it is a part of the composite primary key and used for the conflict target + l1_timestamp: fragment("EXCLUDED.l1_timestamp"), + l1_transaction_hash: fragment("EXCLUDED.l1_transaction_hash"), + l1_block_number: fragment("EXCLUDED.l1_block_number"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", we.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", we.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.l1_timestamp, EXCLUDED.l1_transaction_hash, EXCLUDED.l1_block_number) IS DISTINCT FROM (?, ?, ?)", + we.l1_timestamp, + we.l1_transaction_hash, + we.l1_block_number + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawals.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawals.ex new file mode 100644 index 000000000000..450c97b07017 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawals.ex @@ -0,0 +1,104 @@ +defmodule Explorer.Chain.Import.Runner.Optimism.Withdrawals do + @moduledoc """ + Bulk imports `t:Explorer.Chain.OptimismWithdrawal.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Optimism.Withdrawal, as: OptimismWithdrawal + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [OptimismWithdrawal.t()] + + @impl Import.Runner + def ecto_schema_module, do: OptimismWithdrawal + + @impl Import.Runner + def option_key, do: :optimism_withdrawals + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_withdrawals, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :optimism_withdrawals, + :optimism_withdrawals + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [OptimismWithdrawal.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce OptimismWithdrawal ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.msg_nonce) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: OptimismWithdrawal, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :msg_nonce, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + withdrawal in OptimismWithdrawal, + update: [ + set: [ + # don't update `msg_nonce` as it is a primary key and used for the conflict target + hash: fragment("EXCLUDED.hash"), + l2_transaction_hash: fragment("EXCLUDED.l2_transaction_hash"), + l2_block_number: fragment("EXCLUDED.l2_block_number"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", withdrawal.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", withdrawal.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.hash, EXCLUDED.l2_transaction_hash, EXCLUDED.l2_block_number) IS DISTINCT FROM (?, ?, ?)", + withdrawal.hash, + withdrawal.l2_transaction_hash, + withdrawal.l2_block_number + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/batch_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/batch_transactions.ex new file mode 100644 index 000000000000..c330da10e689 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/batch_transactions.ex @@ -0,0 +1,79 @@ +defmodule Explorer.Chain.Import.Runner.PolygonZkevm.BatchTransactions do + @moduledoc """ + Bulk imports `t:Explorer.Chain.PolygonZkevm.BatchTransaction.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.PolygonZkevm.BatchTransaction + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [BatchTransaction.t()] + + @impl Import.Runner + def ecto_schema_module, do: BatchTransaction + + @impl Import.Runner + def option_key, do: :polygon_zkevm_batch_transactions + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_polygon_zkevm_batch_transactions, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :polygon_zkevm_batch_transactions, + :polygon_zkevm_batch_transactions + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [BatchTransaction.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce PolygonZkevm.BatchTransaction ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: BatchTransaction, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :hash, + on_conflict: :nothing + ) + + {:ok, inserted} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/bridge_l1_tokens.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/bridge_l1_tokens.ex new file mode 100644 index 000000000000..03ed1bd5783c --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/bridge_l1_tokens.ex @@ -0,0 +1,101 @@ +defmodule Explorer.Chain.Import.Runner.PolygonZkevm.BridgeL1Tokens do + @moduledoc """ + Bulk imports `t:Explorer.Chain.PolygonZkevm.BridgeL1Token.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.PolygonZkevm.BridgeL1Token + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [BridgeL1Token.t()] + + @impl Import.Runner + def ecto_schema_module, do: BridgeL1Token + + @impl Import.Runner + def option_key, do: :polygon_zkevm_bridge_l1_tokens + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_polygon_zkevm_bridge_l1_tokens, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :polygon_zkevm_bridge_l1_tokens, + :polygon_zkevm_bridge_l1_tokens + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [BridgeL1Token.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce BridgeL1Token ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.address}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: :address, + on_conflict: on_conflict, + for: BridgeL1Token, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + t in BridgeL1Token, + update: [ + set: [ + decimals: fragment("EXCLUDED.decimals"), + symbol: fragment("EXCLUDED.symbol"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", t.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", t.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.decimals, EXCLUDED.symbol) IS DISTINCT FROM (?, ?)", + t.decimals, + t.symbol + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/bridge_operations.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/bridge_operations.ex new file mode 100644 index 000000000000..6cd724fe70cb --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/bridge_operations.ex @@ -0,0 +1,115 @@ +defmodule Explorer.Chain.Import.Runner.PolygonZkevm.BridgeOperations do + @moduledoc """ + Bulk imports `t:Explorer.Chain.PolygonZkevm.Bridge.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.PolygonZkevm.Bridge, as: PolygonZkevmBridge + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [PolygonZkevmBridge.t()] + + @impl Import.Runner + def ecto_schema_module, do: PolygonZkevmBridge + + @impl Import.Runner + def option_key, do: :polygon_zkevm_bridge_operations + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_polygon_zkevm_bridge_operations, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :polygon_zkevm_bridge_operations, + :polygon_zkevm_bridge_operations + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [PolygonZkevmBridge.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce PolygonZkevmBridge ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.type, &1.index}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:type, :index], + on_conflict: on_conflict, + for: PolygonZkevmBridge, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + op in PolygonZkevmBridge, + update: [ + set: [ + # Don't update `type` as it is part of the composite primary key and used for the conflict target + # Don't update `index` as it is part of the composite primary key and used for the conflict target + l1_transaction_hash: fragment("COALESCE(EXCLUDED.l1_transaction_hash, ?)", op.l1_transaction_hash), + l2_transaction_hash: fragment("COALESCE(EXCLUDED.l2_transaction_hash, ?)", op.l2_transaction_hash), + l1_token_id: fragment("COALESCE(EXCLUDED.l1_token_id, ?)", op.l1_token_id), + l1_token_address: fragment("COALESCE(EXCLUDED.l1_token_address, ?)", op.l1_token_address), + l2_token_address: fragment("COALESCE(EXCLUDED.l2_token_address, ?)", op.l2_token_address), + amount: fragment("EXCLUDED.amount"), + block_number: fragment("COALESCE(EXCLUDED.block_number, ?)", op.block_number), + block_timestamp: fragment("COALESCE(EXCLUDED.block_timestamp, ?)", op.block_timestamp), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", op.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", op.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.l1_transaction_hash, EXCLUDED.l2_transaction_hash, EXCLUDED.l1_token_id, EXCLUDED.l1_token_address, EXCLUDED.l2_token_address, EXCLUDED.amount, EXCLUDED.block_number, EXCLUDED.block_timestamp) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?)", + op.l1_transaction_hash, + op.l2_transaction_hash, + op.l1_token_id, + op.l1_token_address, + op.l2_token_address, + op.amount, + op.block_number, + op.block_timestamp + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/zkevm/lifecycle_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/lifecycle_transactions.ex similarity index 83% rename from apps/explorer/lib/explorer/chain/import/runner/zkevm/lifecycle_transactions.ex rename to apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/lifecycle_transactions.ex index 7a5e4c132735..3b12c4cd19d9 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/zkevm/lifecycle_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/lifecycle_transactions.ex @@ -1,13 +1,13 @@ -defmodule Explorer.Chain.Import.Runner.Zkevm.LifecycleTransactions do +defmodule Explorer.Chain.Import.Runner.PolygonZkevm.LifecycleTransactions do @moduledoc """ - Bulk imports `t:Explorer.Chain.Zkevm.LifecycleTransaction.t/0`. + Bulk imports `t:Explorer.Chain.PolygonZkevm.LifecycleTransaction.t/0`. """ require Ecto.Query alias Ecto.{Changeset, Multi, Repo} alias Explorer.Chain.Import - alias Explorer.Chain.Zkevm.LifecycleTransaction + alias Explorer.Chain.PolygonZkevm.LifecycleTransaction alias Explorer.Prometheus.Instrumenter import Ecto.Query, only: [from: 2] @@ -23,7 +23,7 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.LifecycleTransactions do def ecto_schema_module, do: LifecycleTransaction @impl Import.Runner - def option_key, do: :zkevm_lifecycle_transactions + def option_key, do: :polygon_zkevm_lifecycle_transactions @impl Import.Runner @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} @@ -44,12 +44,12 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.LifecycleTransactions do |> Map.put_new(:timeout, @timeout) |> Map.put(:timestamps, timestamps) - Multi.run(multi, :insert_zkevm_lifecycle_transactions, fn repo, _ -> + Multi.run(multi, :insert_polygon_zkevm_lifecycle_transactions, fn repo, _ -> Instrumenter.block_import_stage_runner( fn -> insert(repo, changes_list, insert_options) end, :block_referencing, - :zkevm_lifecycle_transactions, - :zkevm_lifecycle_transactions + :polygon_zkevm_lifecycle_transactions, + :polygon_zkevm_lifecycle_transactions ) end) end @@ -63,7 +63,7 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.LifecycleTransactions do def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - # Enforce Zkevm.LifecycleTransaction ShareLocks order (see docs: sharelock.md) + # Enforce PolygonZkevm.LifecycleTransaction ShareLocks order (see docs: sharelock.md) ordered_changes_list = Enum.sort_by(changes_list, & &1.id) {:ok, inserted} = diff --git a/apps/explorer/lib/explorer/chain/import/runner/zkevm/transaction_batches.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/transaction_batches.ex similarity index 86% rename from apps/explorer/lib/explorer/chain/import/runner/zkevm/transaction_batches.ex rename to apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/transaction_batches.ex index db9f2771ea40..e2b8930b1828 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/zkevm/transaction_batches.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/polygon_zkevm/transaction_batches.ex @@ -1,13 +1,13 @@ -defmodule Explorer.Chain.Import.Runner.Zkevm.TransactionBatches do +defmodule Explorer.Chain.Import.Runner.PolygonZkevm.TransactionBatches do @moduledoc """ - Bulk imports `t:Explorer.Chain.Zkevm.TransactionBatch.t/0`. + Bulk imports `t:Explorer.Chain.PolygonZkevm.TransactionBatch.t/0`. """ require Ecto.Query alias Ecto.{Changeset, Multi, Repo} alias Explorer.Chain.Import - alias Explorer.Chain.Zkevm.TransactionBatch + alias Explorer.Chain.PolygonZkevm.TransactionBatch alias Explorer.Prometheus.Instrumenter import Ecto.Query, only: [from: 2] @@ -23,7 +23,7 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.TransactionBatches do def ecto_schema_module, do: TransactionBatch @impl Import.Runner - def option_key, do: :zkevm_transaction_batches + def option_key, do: :polygon_zkevm_transaction_batches @impl Import.Runner @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} @@ -44,12 +44,12 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.TransactionBatches do |> Map.put_new(:timeout, @timeout) |> Map.put(:timestamps, timestamps) - Multi.run(multi, :insert_zkevm_transaction_batches, fn repo, _ -> + Multi.run(multi, :insert_polygon_zkevm_transaction_batches, fn repo, _ -> Instrumenter.block_import_stage_runner( fn -> insert(repo, changes_list, insert_options) end, :block_referencing, - :zkevm_transaction_batches, - :zkevm_transaction_batches + :polygon_zkevm_transaction_batches, + :polygon_zkevm_transaction_batches ) end) end @@ -63,7 +63,7 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.TransactionBatches do def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - # Enforce Zkevm.TransactionBatch ShareLocks order (see docs: sharelock.md) + # Enforce PolygonZkevm.TransactionBatch ShareLocks order (see docs: sharelock.md) ordered_changes_list = Enum.sort_by(changes_list, & &1.number) {:ok, inserted} = diff --git a/apps/explorer/lib/explorer/chain/import/runner/shibarium/bridge_operations.ex b/apps/explorer/lib/explorer/chain/import/runner/shibarium/bridge_operations.ex new file mode 100644 index 000000000000..b7cd680ae231 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/shibarium/bridge_operations.ex @@ -0,0 +1,119 @@ +defmodule Explorer.Chain.Import.Runner.Shibarium.BridgeOperations do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Shibarium.Bridge.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Shibarium.Bridge, as: ShibariumBridge + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [ShibariumBridge.t()] + + @impl Import.Runner + def ecto_schema_module, do: ShibariumBridge + + @impl Import.Runner + def option_key, do: :shibarium_bridge_operations + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_shibarium_bridge_operations, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :shibarium_bridge_operations, + :shibarium_bridge_operations + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [ShibariumBridge.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce ShibariumBridge ShareLocks order (see docs: sharelock.md) + ordered_changes_list = + Enum.sort_by(changes_list, &{&1.operation_hash, &1.l1_transaction_hash, &1.l2_transaction_hash}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:operation_hash, :l1_transaction_hash, :l2_transaction_hash], + on_conflict: on_conflict, + for: ShibariumBridge, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + op in ShibariumBridge, + update: [ + set: [ + # Don't update `operation_hash` as it is part of the composite primary key and used for the conflict target + # Don't update `l1_transaction_hash` as it is part of the composite primary key and used for the conflict target + # Don't update `l2_transaction_hash` as it is part of the composite primary key and used for the conflict target + # Don't update `operation_type` as it is not changed + user: fragment("EXCLUDED.user"), + amount_or_id: fragment("EXCLUDED.amount_or_id"), + erc1155_ids: fragment("EXCLUDED.erc1155_ids"), + erc1155_amounts: fragment("EXCLUDED.erc1155_amounts"), + l1_block_number: fragment("EXCLUDED.l1_block_number"), + l2_block_number: fragment("EXCLUDED.l2_block_number"), + token_type: fragment("EXCLUDED.token_type"), + timestamp: fragment("EXCLUDED.timestamp"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", op.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", op.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.user, EXCLUDED.amount_or_id, EXCLUDED.erc1155_ids, EXCLUDED.erc1155_amounts, EXCLUDED.operation_type, EXCLUDED.l1_block_number, EXCLUDED.l2_block_number, EXCLUDED.token_type, EXCLUDED.timestamp) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?)", + op.user, + op.amount_or_id, + op.erc1155_ids, + op.erc1155_amounts, + op.operation_type, + op.l1_block_number, + op.l2_block_number, + op.token_type, + op.timestamp + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex index f50e98691e61..2dac07465ebc 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex @@ -90,18 +90,22 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do to_address_hash: fragment("EXCLUDED.to_address_hash"), token_contract_address_hash: fragment("EXCLUDED.token_contract_address_hash"), token_ids: fragment("EXCLUDED.token_ids"), + token_type: fragment("EXCLUDED.token_type"), + block_consensus: fragment("EXCLUDED.block_consensus"), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token_transfer.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token_transfer.updated_at) ] ], where: fragment( - "(EXCLUDED.amount, EXCLUDED.from_address_hash, EXCLUDED.to_address_hash, EXCLUDED.token_contract_address_hash, EXCLUDED.token_ids) IS DISTINCT FROM (?, ? ,? , ?, ?)", + "(EXCLUDED.amount, EXCLUDED.from_address_hash, EXCLUDED.to_address_hash, EXCLUDED.token_contract_address_hash, EXCLUDED.token_ids, EXCLUDED.token_type, EXCLUDED.block_consensus) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", token_transfer.amount, token_transfer.from_address_hash, token_transfer.to_address_hash, token_transfer.token_contract_address_hash, - token_transfer.token_ids + token_transfer.token_ids, + token_transfer.token_type, + token_transfer.block_consensus ) ) end diff --git a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex index d60afaee2d7a..638b59a71095 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex @@ -136,36 +136,73 @@ defmodule Explorer.Chain.Import.Runner.Tokens do ) end - def default_on_conflict do - from( - token in Token, - update: [ - set: [ - name: fragment("EXCLUDED.name"), - symbol: fragment("EXCLUDED.symbol"), - total_supply: fragment("EXCLUDED.total_supply"), - decimals: fragment("EXCLUDED.decimals"), - type: fragment("EXCLUDED.type"), - cataloged: fragment("EXCLUDED.cataloged"), - skip_metadata: fragment("EXCLUDED.skip_metadata"), - # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR - # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` - # Don't update `contract_address_hash` as it is the primary key and used for the conflict target - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.total_supply, EXCLUDED.decimals, EXCLUDED.type, EXCLUDED.cataloged, EXCLUDED.skip_metadata) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", - token.name, - token.symbol, - token.total_supply, - token.decimals, - token.type, - token.cataloged, - token.skip_metadata - ) - ) + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + def default_on_conflict do + from( + token in Token, + update: [ + set: [ + name: fragment("COALESCE(EXCLUDED.name, ?)", token.name), + symbol: fragment("COALESCE(EXCLUDED.symbol, ?)", token.symbol), + total_supply: fragment("COALESCE(EXCLUDED.total_supply, ?)", token.total_supply), + decimals: fragment("COALESCE(EXCLUDED.decimals, ?)", token.decimals), + type: fragment("COALESCE(EXCLUDED.type, ?)", token.type), + cataloged: fragment("COALESCE(EXCLUDED.cataloged, ?)", token.cataloged), + bridged: fragment("COALESCE(EXCLUDED.bridged, ?)", token.bridged), + skip_metadata: fragment("COALESCE(EXCLUDED.skip_metadata, ?)", token.skip_metadata), + # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR + # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` + # Don't update `contract_address_hash` as it is the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.total_supply, EXCLUDED.decimals, EXCLUDED.type, EXCLUDED.cataloged, EXCLUDED.bridged, EXCLUDED.skip_metadata) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?)", + token.name, + token.symbol, + token.total_supply, + token.decimals, + token.type, + token.cataloged, + token.bridged, + token.skip_metadata + ) + ) + end + else + def default_on_conflict do + from( + token in Token, + update: [ + set: [ + name: fragment("COALESCE(EXCLUDED.name, ?)", token.name), + symbol: fragment("COALESCE(EXCLUDED.symbol, ?)", token.symbol), + total_supply: fragment("COALESCE(EXCLUDED.total_supply, ?)", token.total_supply), + decimals: fragment("COALESCE(EXCLUDED.decimals, ?)", token.decimals), + type: fragment("COALESCE(EXCLUDED.type, ?)", token.type), + cataloged: fragment("COALESCE(EXCLUDED.cataloged, ?)", token.cataloged), + skip_metadata: fragment("COALESCE(EXCLUDED.skip_metadata, ?)", token.skip_metadata), + # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR + # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` + # Don't update `contract_address_hash` as it is the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.total_supply, EXCLUDED.decimals, EXCLUDED.type, EXCLUDED.cataloged, EXCLUDED.skip_metadata) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + token.name, + token.symbol, + token.total_supply, + token.decimals, + token.type, + token.cataloged, + token.skip_metadata + ) + ) + end end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index 077bc41a286d..1f0098afc2f3 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -8,7 +8,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do import Ecto.Query, only: [from: 2] alias Ecto.{Multi, Repo} - alias Explorer.Chain.{Block, Hash, Import, Transaction} + alias Explorer.Chain.{Block, Hash, Import, TokenTransfer, Transaction} alias Explorer.Chain.Import.Runner.TokenTransfers alias Explorer.Prometheus.Instrumenter alias Explorer.Utility.MissingRangesManipulator @@ -108,160 +108,250 @@ defmodule Explorer.Chain.Import.Runner.Transactions do end defp default_on_conflict do - if System.get_env("CHAIN_TYPE") == "suave" do - from( - transaction in Transaction, - update: [ - set: [ - block_hash: fragment("EXCLUDED.block_hash"), - old_block_hash: transaction.block_hash, - block_number: fragment("EXCLUDED.block_number"), - created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), - created_contract_code_indexed_at: fragment("EXCLUDED.created_contract_code_indexed_at"), - cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"), - error: fragment("EXCLUDED.error"), - from_address_hash: fragment("EXCLUDED.from_address_hash"), - gas: fragment("EXCLUDED.gas"), - gas_price: fragment("EXCLUDED.gas_price"), - gas_used: fragment("EXCLUDED.gas_used"), - index: fragment("EXCLUDED.index"), - input: fragment("EXCLUDED.input"), - nonce: fragment("EXCLUDED.nonce"), - r: fragment("EXCLUDED.r"), - s: fragment("EXCLUDED.s"), - status: fragment("EXCLUDED.status"), - to_address_hash: fragment("EXCLUDED.to_address_hash"), - v: fragment("EXCLUDED.v"), - value: fragment("EXCLUDED.value"), - earliest_processing_start: fragment("EXCLUDED.earliest_processing_start"), - revert_reason: fragment("EXCLUDED.revert_reason"), - max_priority_fee_per_gas: fragment("EXCLUDED.max_priority_fee_per_gas"), - max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), - type: fragment("EXCLUDED.type"), - execution_node_hash: fragment("EXCLUDED.execution_node_hash"), - wrapped_type: fragment("EXCLUDED.wrapped_type"), - wrapped_nonce: fragment("EXCLUDED.wrapped_nonce"), - wrapped_to_address_hash: fragment("EXCLUDED.wrapped_to_address_hash"), - wrapped_gas: fragment("EXCLUDED.wrapped_gas"), - wrapped_gas_price: fragment("EXCLUDED.wrapped_gas_price"), - wrapped_max_priority_fee_per_gas: fragment("EXCLUDED.wrapped_max_priority_fee_per_gas"), - wrapped_max_fee_per_gas: fragment("EXCLUDED.wrapped_max_fee_per_gas"), - wrapped_value: fragment("EXCLUDED.wrapped_value"), - wrapped_input: fragment("EXCLUDED.wrapped_input"), - wrapped_v: fragment("EXCLUDED.wrapped_v"), - wrapped_r: fragment("EXCLUDED.wrapped_r"), - wrapped_s: fragment("EXCLUDED.wrapped_s"), - wrapped_hash: fragment("EXCLUDED.wrapped_hash"), - # Don't update `hash` as it is part of the primary key and used for the conflict target - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.execution_node_hash, EXCLUDED.wrapped_type, EXCLUDED.wrapped_nonce, EXCLUDED.wrapped_to_address_hash, EXCLUDED.wrapped_gas, EXCLUDED.wrapped_gas_price, EXCLUDED.wrapped_max_priority_fee_per_gas, EXCLUDED.wrapped_max_fee_per_gas, EXCLUDED.wrapped_value, EXCLUDED.wrapped_input, EXCLUDED.wrapped_v, EXCLUDED.wrapped_r, EXCLUDED.wrapped_s, EXCLUDED.wrapped_hash) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - transaction.block_hash, - transaction.block_number, - transaction.created_contract_address_hash, - transaction.created_contract_code_indexed_at, - transaction.cumulative_gas_used, - transaction.from_address_hash, - transaction.gas, - transaction.gas_price, - transaction.gas_used, - transaction.index, - transaction.input, - transaction.nonce, - transaction.r, - transaction.s, - transaction.status, - transaction.to_address_hash, - transaction.v, - transaction.value, - transaction.earliest_processing_start, - transaction.revert_reason, - transaction.max_priority_fee_per_gas, - transaction.max_fee_per_gas, - transaction.type, - transaction.execution_node_hash, - transaction.wrapped_type, - transaction.wrapped_nonce, - transaction.wrapped_to_address_hash, - transaction.wrapped_gas, - transaction.wrapped_gas_price, - transaction.wrapped_max_priority_fee_per_gas, - transaction.wrapped_max_fee_per_gas, - transaction.wrapped_value, - transaction.wrapped_input, - transaction.wrapped_v, - transaction.wrapped_r, - transaction.wrapped_s, - transaction.wrapped_hash - ) - ) - else - from( - transaction in Transaction, - update: [ - set: [ - block_hash: fragment("EXCLUDED.block_hash"), - old_block_hash: transaction.block_hash, - block_number: fragment("EXCLUDED.block_number"), - created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), - created_contract_code_indexed_at: fragment("EXCLUDED.created_contract_code_indexed_at"), - cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"), - error: fragment("EXCLUDED.error"), - from_address_hash: fragment("EXCLUDED.from_address_hash"), - gas: fragment("EXCLUDED.gas"), - gas_price: fragment("EXCLUDED.gas_price"), - gas_used: fragment("EXCLUDED.gas_used"), - index: fragment("EXCLUDED.index"), - input: fragment("EXCLUDED.input"), - nonce: fragment("EXCLUDED.nonce"), - r: fragment("EXCLUDED.r"), - s: fragment("EXCLUDED.s"), - status: fragment("EXCLUDED.status"), - to_address_hash: fragment("EXCLUDED.to_address_hash"), - v: fragment("EXCLUDED.v"), - value: fragment("EXCLUDED.value"), - earliest_processing_start: fragment("EXCLUDED.earliest_processing_start"), - revert_reason: fragment("EXCLUDED.revert_reason"), - max_priority_fee_per_gas: fragment("EXCLUDED.max_priority_fee_per_gas"), - max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), - type: fragment("EXCLUDED.type"), - # Don't update `hash` as it is part of the primary key and used for the conflict target - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - transaction.block_hash, - transaction.block_number, - transaction.created_contract_address_hash, - transaction.created_contract_code_indexed_at, - transaction.cumulative_gas_used, - transaction.from_address_hash, - transaction.gas, - transaction.gas_price, - transaction.gas_used, - transaction.index, - transaction.input, - transaction.nonce, - transaction.r, - transaction.s, - transaction.status, - transaction.to_address_hash, - transaction.v, - transaction.value, - transaction.earliest_processing_start, - transaction.revert_reason, - transaction.max_priority_fee_per_gas, - transaction.max_fee_per_gas, - transaction.type - ) - ) + case Application.get_env(:explorer, :chain_type) do + "suave" -> + from( + transaction in Transaction, + update: [ + set: [ + block_hash: fragment("EXCLUDED.block_hash"), + old_block_hash: transaction.block_hash, + block_number: fragment("EXCLUDED.block_number"), + block_consensus: fragment("EXCLUDED.block_consensus"), + block_timestamp: fragment("EXCLUDED.block_timestamp"), + created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), + created_contract_code_indexed_at: fragment("EXCLUDED.created_contract_code_indexed_at"), + cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"), + error: fragment("EXCLUDED.error"), + from_address_hash: fragment("EXCLUDED.from_address_hash"), + gas: fragment("EXCLUDED.gas"), + gas_price: fragment("EXCLUDED.gas_price"), + gas_used: fragment("EXCLUDED.gas_used"), + index: fragment("EXCLUDED.index"), + input: fragment("EXCLUDED.input"), + nonce: fragment("EXCLUDED.nonce"), + r: fragment("EXCLUDED.r"), + s: fragment("EXCLUDED.s"), + status: fragment("EXCLUDED.status"), + to_address_hash: fragment("EXCLUDED.to_address_hash"), + v: fragment("EXCLUDED.v"), + value: fragment("EXCLUDED.value"), + earliest_processing_start: fragment("EXCLUDED.earliest_processing_start"), + revert_reason: fragment("EXCLUDED.revert_reason"), + max_priority_fee_per_gas: fragment("EXCLUDED.max_priority_fee_per_gas"), + max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), + type: fragment("EXCLUDED.type"), + execution_node_hash: fragment("EXCLUDED.execution_node_hash"), + wrapped_type: fragment("EXCLUDED.wrapped_type"), + wrapped_nonce: fragment("EXCLUDED.wrapped_nonce"), + wrapped_to_address_hash: fragment("EXCLUDED.wrapped_to_address_hash"), + wrapped_gas: fragment("EXCLUDED.wrapped_gas"), + wrapped_gas_price: fragment("EXCLUDED.wrapped_gas_price"), + wrapped_max_priority_fee_per_gas: fragment("EXCLUDED.wrapped_max_priority_fee_per_gas"), + wrapped_max_fee_per_gas: fragment("EXCLUDED.wrapped_max_fee_per_gas"), + wrapped_value: fragment("EXCLUDED.wrapped_value"), + wrapped_input: fragment("EXCLUDED.wrapped_input"), + wrapped_v: fragment("EXCLUDED.wrapped_v"), + wrapped_r: fragment("EXCLUDED.wrapped_r"), + wrapped_s: fragment("EXCLUDED.wrapped_s"), + wrapped_hash: fragment("EXCLUDED.wrapped_hash"), + # Don't update `hash` as it is part of the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.execution_node_hash, EXCLUDED.wrapped_type, EXCLUDED.wrapped_nonce, EXCLUDED.wrapped_to_address_hash, EXCLUDED.wrapped_gas, EXCLUDED.wrapped_gas_price, EXCLUDED.wrapped_max_priority_fee_per_gas, EXCLUDED.wrapped_max_fee_per_gas, EXCLUDED.wrapped_value, EXCLUDED.wrapped_input, EXCLUDED.wrapped_v, EXCLUDED.wrapped_r, EXCLUDED.wrapped_s, EXCLUDED.wrapped_hash) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + transaction.block_hash, + transaction.block_number, + transaction.block_consensus, + transaction.block_timestamp, + transaction.created_contract_address_hash, + transaction.created_contract_code_indexed_at, + transaction.cumulative_gas_used, + transaction.from_address_hash, + transaction.gas, + transaction.gas_price, + transaction.gas_used, + transaction.index, + transaction.input, + transaction.nonce, + transaction.r, + transaction.s, + transaction.status, + transaction.to_address_hash, + transaction.v, + transaction.value, + transaction.earliest_processing_start, + transaction.revert_reason, + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas, + transaction.type, + transaction.execution_node_hash, + transaction.wrapped_type, + transaction.wrapped_nonce, + transaction.wrapped_to_address_hash, + transaction.wrapped_gas, + transaction.wrapped_gas_price, + transaction.wrapped_max_priority_fee_per_gas, + transaction.wrapped_max_fee_per_gas, + transaction.wrapped_value, + transaction.wrapped_input, + transaction.wrapped_v, + transaction.wrapped_r, + transaction.wrapped_s, + transaction.wrapped_hash + ) + ) + + "optimism" -> + from( + transaction in Transaction, + update: [ + set: [ + block_hash: fragment("EXCLUDED.block_hash"), + old_block_hash: transaction.block_hash, + block_number: fragment("EXCLUDED.block_number"), + block_consensus: fragment("EXCLUDED.block_consensus"), + block_timestamp: fragment("EXCLUDED.block_timestamp"), + created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), + created_contract_code_indexed_at: fragment("EXCLUDED.created_contract_code_indexed_at"), + cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"), + error: fragment("EXCLUDED.error"), + from_address_hash: fragment("EXCLUDED.from_address_hash"), + gas: fragment("EXCLUDED.gas"), + gas_price: fragment("EXCLUDED.gas_price"), + gas_used: fragment("EXCLUDED.gas_used"), + index: fragment("EXCLUDED.index"), + input: fragment("EXCLUDED.input"), + nonce: fragment("EXCLUDED.nonce"), + r: fragment("EXCLUDED.r"), + s: fragment("EXCLUDED.s"), + status: fragment("EXCLUDED.status"), + to_address_hash: fragment("EXCLUDED.to_address_hash"), + v: fragment("EXCLUDED.v"), + value: fragment("EXCLUDED.value"), + earliest_processing_start: fragment("EXCLUDED.earliest_processing_start"), + revert_reason: fragment("EXCLUDED.revert_reason"), + max_priority_fee_per_gas: fragment("EXCLUDED.max_priority_fee_per_gas"), + max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), + type: fragment("EXCLUDED.type"), + l1_fee: fragment("EXCLUDED.l1_fee"), + l1_fee_scalar: fragment("EXCLUDED.l1_fee_scalar"), + l1_gas_price: fragment("EXCLUDED.l1_gas_price"), + l1_gas_used: fragment("EXCLUDED.l1_gas_used"), + l1_tx_origin: fragment("EXCLUDED.l1_tx_origin"), + l1_block_number: fragment("EXCLUDED.l1_block_number"), + # Don't update `hash` as it is part of the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.l1_fee_scalar, EXCLUDED.l1_gas_price, EXCLUDED.l1_gas_used, EXCLUDED.l1_tx_origin, EXCLUDED.l1_block_number) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + transaction.block_hash, + transaction.block_number, + transaction.block_consensus, + transaction.block_timestamp, + transaction.created_contract_address_hash, + transaction.created_contract_code_indexed_at, + transaction.cumulative_gas_used, + transaction.from_address_hash, + transaction.gas, + transaction.gas_price, + transaction.gas_used, + transaction.index, + transaction.input, + transaction.nonce, + transaction.r, + transaction.s, + transaction.status, + transaction.to_address_hash, + transaction.v, + transaction.value, + transaction.earliest_processing_start, + transaction.revert_reason, + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas, + transaction.type, + transaction.l1_fee, + transaction.l1_fee_scalar, + transaction.l1_gas_price, + transaction.l1_gas_used, + transaction.l1_tx_origin, + transaction.l1_block_number + ) + ) + + _ -> + from( + transaction in Transaction, + update: [ + set: [ + block_hash: fragment("EXCLUDED.block_hash"), + old_block_hash: transaction.block_hash, + block_number: fragment("EXCLUDED.block_number"), + block_consensus: fragment("EXCLUDED.block_consensus"), + block_timestamp: fragment("EXCLUDED.block_timestamp"), + created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), + created_contract_code_indexed_at: fragment("EXCLUDED.created_contract_code_indexed_at"), + cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"), + error: fragment("EXCLUDED.error"), + from_address_hash: fragment("EXCLUDED.from_address_hash"), + gas: fragment("EXCLUDED.gas"), + gas_price: fragment("EXCLUDED.gas_price"), + gas_used: fragment("EXCLUDED.gas_used"), + index: fragment("EXCLUDED.index"), + input: fragment("EXCLUDED.input"), + nonce: fragment("EXCLUDED.nonce"), + r: fragment("EXCLUDED.r"), + s: fragment("EXCLUDED.s"), + status: fragment("EXCLUDED.status"), + to_address_hash: fragment("EXCLUDED.to_address_hash"), + v: fragment("EXCLUDED.v"), + value: fragment("EXCLUDED.value"), + earliest_processing_start: fragment("EXCLUDED.earliest_processing_start"), + revert_reason: fragment("EXCLUDED.revert_reason"), + max_priority_fee_per_gas: fragment("EXCLUDED.max_priority_fee_per_gas"), + max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), + type: fragment("EXCLUDED.type"), + # Don't update `hash` as it is part of the primary key and used for the conflict target + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + transaction.block_hash, + transaction.block_number, + transaction.block_consensus, + transaction.block_timestamp, + transaction.created_contract_address_hash, + transaction.created_contract_code_indexed_at, + transaction.cumulative_gas_used, + transaction.from_address_hash, + transaction.gas, + transaction.gas_price, + transaction.gas_used, + transaction.index, + transaction.input, + transaction.nonce, + transaction.r, + transaction.s, + transaction.status, + transaction.to_address_hash, + transaction.v, + transaction.value, + transaction.earliest_processing_start, + transaction.revert_reason, + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas, + transaction.type + ) + ) end end @@ -291,14 +381,20 @@ defmodule Explorer.Chain.Import.Runner.Transactions do ), on: transaction.hash == new_transaction.hash, where: transaction.block_hash != new_transaction.block_hash, - select: transaction.block_hash + select: %{hash: transaction.hash, block_hash: transaction.block_hash} ) block_hashes = blocks_with_recollated_transactions |> repo.all() + |> Enum.map(fn %{block_hash: block_hash} -> block_hash end) |> Enum.uniq() + transaction_hashes = + blocks_with_recollated_transactions + |> repo.all() + |> Enum.map(fn %{hash: hash} -> hash end) + if Enum.empty?(block_hashes) do {:ok, []} else @@ -357,5 +453,42 @@ defmodule Explorer.Chain.Import.Runner.Transactions do {:error, %{exception: postgrex_error, block_hashes: block_hashes}} end end + + if Enum.empty?(transaction_hashes) do + {:ok, []} + else + query = + from( + transaction in Transaction, + where: transaction.hash in ^transaction_hashes, + # Enforce Block ShareLocks order (see docs: sharelocks.md) + order_by: [asc: transaction.hash], + lock: "FOR UPDATE" + ) + + try do + {_, result} = + repo.update_all( + from(transaction in Transaction, join: s in subquery(query), on: transaction.hash == s.hash), + [set: [block_consensus: false, updated_at: updated_at]], + timeout: timeout + ) + + {_, _result} = + repo.update_all( + from(token_transfer in TokenTransfer, + join: s in subquery(query), + on: token_transfer.transaction_hash == s.hash + ), + [set: [block_consensus: false, updated_at: updated_at]], + timeout: timeout + ) + + {:ok, result} + rescue + postgrex_error in Postgrex.Error -> + {:error, %{exception: postgrex_error, transaction_hashes: transaction_hashes}} + end + end end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/zksync/batch_blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/zksync/batch_blocks.ex new file mode 100644 index 000000000000..33d075e93588 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/zksync/batch_blocks.ex @@ -0,0 +1,79 @@ +defmodule Explorer.Chain.Import.Runner.ZkSync.BatchBlocks do + @moduledoc """ + Bulk imports `t:Explorer.Chain.ZkSync.BatchBlock.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.ZkSync.BatchBlock + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [BatchBlock.t()] + + @impl Import.Runner + def ecto_schema_module, do: BatchBlock + + @impl Import.Runner + def option_key, do: :zksync_batch_blocks + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_zksync_batch_blocks, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :zksync_batch_blocks, + :zksync_batch_blocks + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [BatchBlock.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce ZkSync.BatchBlock ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: BatchBlock, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :hash, + on_conflict: :nothing + ) + + {:ok, inserted} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/zkevm/batch_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/zksync/batch_transactions.ex similarity index 80% rename from apps/explorer/lib/explorer/chain/import/runner/zkevm/batch_transactions.ex rename to apps/explorer/lib/explorer/chain/import/runner/zksync/batch_transactions.ex index 2df1223945a1..720519a10093 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/zkevm/batch_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/zksync/batch_transactions.ex @@ -1,13 +1,13 @@ -defmodule Explorer.Chain.Import.Runner.Zkevm.BatchTransactions do +defmodule Explorer.Chain.Import.Runner.ZkSync.BatchTransactions do @moduledoc """ - Bulk imports `t:Explorer.Chain.Zkevm.BatchTransaction.t/0`. + Bulk imports `t:Explorer.Chain.ZkSync.BatchTransaction.t/0`. """ require Ecto.Query alias Ecto.{Changeset, Multi, Repo} alias Explorer.Chain.Import - alias Explorer.Chain.Zkevm.BatchTransaction + alias Explorer.Chain.ZkSync.BatchTransaction alias Explorer.Prometheus.Instrumenter @behaviour Import.Runner @@ -21,7 +21,7 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.BatchTransactions do def ecto_schema_module, do: BatchTransaction @impl Import.Runner - def option_key, do: :zkevm_batch_transactions + def option_key, do: :zksync_batch_transactions @impl Import.Runner @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} @@ -42,12 +42,12 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.BatchTransactions do |> Map.put_new(:timeout, @timeout) |> Map.put(:timestamps, timestamps) - Multi.run(multi, :insert_zkevm_batch_transactions, fn repo, _ -> + Multi.run(multi, :insert_zksync_batch_transactions, fn repo, _ -> Instrumenter.block_import_stage_runner( fn -> insert(repo, changes_list, insert_options) end, :block_referencing, - :zkevm_batch_transactions, - :zkevm_batch_transactions + :zksync_batch_transactions, + :zksync_batch_transactions ) end) end @@ -59,7 +59,7 @@ defmodule Explorer.Chain.Import.Runner.Zkevm.BatchTransactions do {:ok, [BatchTransaction.t()]} | {:error, [Changeset.t()]} def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do - # Enforce Zkevm.BatchTransaction ShareLocks order (see docs: sharelock.md) + # Enforce ZkSync.BatchTransaction ShareLocks order (see docs: sharelock.md) ordered_changes_list = Enum.sort_by(changes_list, & &1.hash) {:ok, inserted} = diff --git a/apps/explorer/lib/explorer/chain/import/runner/zksync/lifecycle_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/zksync/lifecycle_transactions.ex new file mode 100644 index 000000000000..b5b5e74ee89d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/zksync/lifecycle_transactions.ex @@ -0,0 +1,103 @@ +defmodule Explorer.Chain.Import.Runner.ZkSync.LifecycleTransactions do + @moduledoc """ + Bulk imports `t:Explorer.Chain.ZkSync.LifecycleTransaction.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.ZkSync.LifecycleTransaction + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [LifecycleTransaction.t()] + + @impl Import.Runner + def ecto_schema_module, do: LifecycleTransaction + + @impl Import.Runner + def option_key, do: :zksync_lifecycle_transactions + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_zksync_lifecycle_transactions, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :zksync_lifecycle_transactions, + :zksync_lifecycle_transactions + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [LifecycleTransaction.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce ZkSync.LifecycleTransaction ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.id) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: LifecycleTransaction, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :hash, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + tx in LifecycleTransaction, + update: [ + set: [ + # don't update `id` as it is a primary key + # don't update `hash` as it is a unique index and used for the conflict target + timestamp: fragment("EXCLUDED.timestamp"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tx.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tx.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.timestamp) IS DISTINCT FROM (?)", + tx.timestamp + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/zksync/transaction_batches.ex b/apps/explorer/lib/explorer/chain/import/runner/zksync/transaction_batches.ex new file mode 100644 index 000000000000..2c4639a43a63 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/zksync/transaction_batches.ex @@ -0,0 +1,122 @@ +defmodule Explorer.Chain.Import.Runner.ZkSync.TransactionBatches do + @moduledoc """ + Bulk imports `t:Explorer.Chain.ZkSync.TransactionBatch.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.ZkSync.TransactionBatch + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [TransactionBatch.t()] + + @impl Import.Runner + def ecto_schema_module, do: TransactionBatch + + @impl Import.Runner + def option_key, do: :zksync_transaction_batches + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_zksync_transaction_batches, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :zksync_transaction_batches, + :zksync_transaction_batches + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [TransactionBatch.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce ZkSync.TransactionBatch ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.number) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: TransactionBatch, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :number, + on_conflict: on_conflict + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + tb in TransactionBatch, + update: [ + set: [ + # don't update `number` as it is a primary key and used for the conflict target + timestamp: fragment("EXCLUDED.timestamp"), + l1_tx_count: fragment("EXCLUDED.l1_tx_count"), + l2_tx_count: fragment("EXCLUDED.l2_tx_count"), + root_hash: fragment("EXCLUDED.root_hash"), + l1_gas_price: fragment("EXCLUDED.l1_gas_price"), + l2_fair_gas_price: fragment("EXCLUDED.l2_fair_gas_price"), + start_block: fragment("EXCLUDED.start_block"), + end_block: fragment("EXCLUDED.end_block"), + commit_id: fragment("EXCLUDED.commit_id"), + prove_id: fragment("EXCLUDED.prove_id"), + execute_id: fragment("EXCLUDED.execute_id"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", tb.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", tb.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.timestamp, EXCLUDED.l1_tx_count, EXCLUDED.l2_tx_count, EXCLUDED.root_hash, EXCLUDED.l1_gas_price, EXCLUDED.l2_fair_gas_price, EXCLUDED.start_block, EXCLUDED.end_block, EXCLUDED.commit_id, EXCLUDED.prove_id, EXCLUDED.execute_id) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + tb.timestamp, + tb.l1_tx_count, + tb.l2_tx_count, + tb.root_hash, + tb.l1_gas_price, + tb.l2_fair_gas_price, + tb.start_block, + tb.end_block, + tb.commit_id, + tb.prove_id, + tb.execute_id + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage.ex b/apps/explorer/lib/explorer/chain/import/stage.ex index dcd3da1cc13e..ed000760ca9b 100644 --- a/apps/explorer/lib/explorer/chain/import/stage.ex +++ b/apps/explorer/lib/explorer/chain/import/stage.ex @@ -14,10 +14,18 @@ defmodule Explorer.Chain.Import.Stage do @type runner_to_changes_list :: %{Runner.t() => Runner.changes_list()} @doc """ - The runners consumed by this stage in `c:multis/0`. The list should be in the order that the runners are executed. + The configured runners consumed by this stage in `c:multis/0`. + The list should be in the order that the runners are executed and depends on chain type. """ @callback runners() :: [Runner.t(), ...] + @doc """ + Returns a list of all possible runners provided by the module. + This list is intended to include all runners, irrespective of chain type or + other configuration options. + """ + @callback all_runners() :: [Runner.t(), ...] + @doc """ Chunks `changes_list` into 1 or more `t:Ecto.Multi.t/0` that can be run in separate transactions. diff --git a/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex index 2d40ad74fb47..72b62a2f9be1 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex @@ -16,6 +16,10 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do Runner.Address.CoinBalancesDaily ] + @impl Stage + def all_runners, + do: runners() + @impl Stage def multis(runner_to_changes_list, options) do {final_multi, final_remaining_runner_to_changes_list} = diff --git a/apps/explorer/lib/explorer/chain/import/stage/addresses.ex b/apps/explorer/lib/explorer/chain/import/stage/addresses.ex index 03c8a5772449..fe91366bf74b 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/addresses.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/addresses.ex @@ -13,6 +13,10 @@ defmodule Explorer.Chain.Import.Stage.Addresses do @impl Stage def runners, do: [@runner] + @impl Stage + def all_runners, + do: runners() + @chunk_size 50 @impl Stage diff --git a/apps/explorer/lib/explorer/chain/import/stage/addresses_blocks_coin_balances.ex b/apps/explorer/lib/explorer/chain/import/stage/addresses_blocks_coin_balances.ex new file mode 100644 index 000000000000..cfa42beaf5f6 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/stage/addresses_blocks_coin_balances.ex @@ -0,0 +1,37 @@ +defmodule Explorer.Chain.Import.Stage.AddressesBlocksCoinBalances do + @moduledoc """ + Import addresses, blocks and balances. + No tables have foreign key to addresses anymore, so it's possible to import addresses along with them. + """ + + alias Explorer.Chain.Import.{Runner, Stage} + + @behaviour Stage + + @addresses_runner Runner.Addresses + + @rest_runners [ + Runner.Address.CoinBalances, + Runner.Blocks, + Runner.Address.CoinBalancesDaily + ] + + @impl Stage + def runners, do: [@addresses_runner | @rest_runners] + + @impl Stage + def all_runners, do: runners() + + @addresses_chunk_size 50 + + @impl Stage + def multis(runner_to_changes_list, options) do + {addresses_multis, remaining_runner_to_changes_list} = + Stage.chunk_every(runner_to_changes_list, Runner.Addresses, @addresses_chunk_size, options) + + {final_multi, final_remaining_runner_to_changes_list} = + Stage.single_multi(@rest_runners, remaining_runner_to_changes_list, options) + + {[final_multi | addresses_multis], final_remaining_runner_to_changes_list} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/block_following.ex b/apps/explorer/lib/explorer/chain/import/stage/block_following.ex index 8abbf9f79b7d..193de566e68e 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/block_following.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/block_following.ex @@ -1,9 +1,7 @@ defmodule Explorer.Chain.Import.Stage.BlockFollowing do @moduledoc """ Imports any tables that follows and cannot be imported at the same time as - those imported by `Explorer.Chain.Import.Stage.Addresses`, - `Explorer.Chain.Import.Stage.AddressReferencing` and - `Explorer.Chain.Import.Stage.BlockReferencing` + those imported by `Explorer.Chain.Import.Stage.AddressesBlocksCoinBalances` and `Explorer.Chain.Import.Stage.BlockReferencing` """ alias Explorer.Chain.Import.{Runner, Stage} @@ -15,10 +13,13 @@ defmodule Explorer.Chain.Import.Stage.BlockFollowing do do: [ Runner.Block.SecondDegreeRelations, Runner.Block.Rewards, - Runner.Address.CurrentTokenBalances, - Runner.TokenInstances + Runner.Address.CurrentTokenBalances ] + @impl Stage + def all_runners, + do: runners() + @impl Stage def multis(runner_to_changes_list, options) do {final_multi, final_remaining_runner_to_changes_list} = diff --git a/apps/explorer/lib/explorer/chain/import/stage/block_pending.ex b/apps/explorer/lib/explorer/chain/import/stage/block_pending.ex index fba315e142d4..6dccdfdf5d10 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/block_pending.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/block_pending.ex @@ -2,9 +2,7 @@ defmodule Explorer.Chain.Import.Stage.BlockPending do @moduledoc """ Imports any tables that uses `Explorer.Chain.PendingBlockOperation` to track progress and cannot be imported at the same time as those imported by - `Explorer.Chain.Import.Stage.Addresses`, - `Explorer.Chain.Import.Stage.AddressReferencing` and - `Explorer.Chain.Import.Stage.BlockReferencing` + `Explorer.Chain.Import.Stage.AddressesBlocksCoinBalances` and `Explorer.Chain.Import.Stage.BlockReferencing` """ alias Explorer.Chain.Import.{Runner, Stage} @@ -17,6 +15,10 @@ defmodule Explorer.Chain.Import.Stage.BlockPending do Runner.InternalTransactions ] + @impl Stage + def all_runners, + do: runners() + @impl Stage def multis(runner_to_changes_list, options) do {final_multi, final_remaining_runner_to_changes_list} = diff --git a/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex index 1589c95a38cb..5f410f1f5a9d 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex @@ -1,8 +1,7 @@ defmodule Explorer.Chain.Import.Stage.BlockReferencing do @moduledoc """ Imports any tables that reference `t:Explorer.Chain.Block.t/0` and that were - imported by `Explorer.Chain.Import.Stage.Addresses` and - `Explorer.Chain.Import.Stage.AddressReferencing`. + imported by `Explorer.Chain.Import.Stage.AddressesBlocksCoinBalances`. """ alias Explorer.Chain.Import.{Runner, Stage} @@ -14,36 +13,83 @@ defmodule Explorer.Chain.Import.Stage.BlockReferencing do Runner.Logs, Runner.Tokens, Runner.TokenTransfers, + Runner.TokenInstances, Runner.Address.TokenBalances, Runner.TransactionActions, Runner.Withdrawals ] + @optimism_runners [ + Runner.Optimism.FrameSequences, + Runner.Optimism.TxnBatches, + Runner.Optimism.OutputRoots, + Runner.Optimism.Deposits, + Runner.Optimism.Withdrawals, + Runner.Optimism.WithdrawalEvents + ] + + @polygon_edge_runners [ + Runner.PolygonEdge.Deposits, + Runner.PolygonEdge.DepositExecutes, + Runner.PolygonEdge.Withdrawals, + Runner.PolygonEdge.WithdrawalExits + ] + + @polygon_zkevm_runners [ + Runner.PolygonZkevm.LifecycleTransactions, + Runner.PolygonZkevm.TransactionBatches, + Runner.PolygonZkevm.BatchTransactions, + Runner.PolygonZkevm.BridgeL1Tokens, + Runner.PolygonZkevm.BridgeOperations + ] + + @zksync_runners [ + Runner.ZkSync.LifecycleTransactions, + Runner.ZkSync.TransactionBatches, + Runner.ZkSync.BatchTransactions, + Runner.ZkSync.BatchBlocks + ] + + @shibarium_runners [ + Runner.Shibarium.BridgeOperations + ] + + @ethereum_runners [ + Runner.Beacon.BlobTransactions + ] + @impl Stage def runners do - case System.get_env("CHAIN_TYPE") do + case Application.get_env(:explorer, :chain_type) do + "optimism" -> + @default_runners ++ @optimism_runners + "polygon_edge" -> - @default_runners ++ - [ - Runner.PolygonEdge.Deposits, - Runner.PolygonEdge.DepositExecutes, - Runner.PolygonEdge.Withdrawals, - Runner.PolygonEdge.WithdrawalExits - ] + @default_runners ++ @polygon_edge_runners "polygon_zkevm" -> - @default_runners ++ - [ - Runner.Zkevm.LifecycleTransactions, - Runner.Zkevm.TransactionBatches, - Runner.Zkevm.BatchTransactions - ] + @default_runners ++ @polygon_zkevm_runners + + "shibarium" -> + @default_runners ++ @shibarium_runners + + "ethereum" -> + @default_runners ++ @ethereum_runners + + "zksync" -> + @default_runners ++ @zksync_runners _ -> @default_runners end end + @impl Stage + def all_runners do + @default_runners ++ + @optimism_runners ++ @polygon_edge_runners ++ @polygon_zkevm_runners ++ @shibarium_runners ++ @zksync_runners + end + @impl Stage def multis(runner_to_changes_list, options) do {final_multi, final_remaining_runner_to_changes_list} = diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index c86bfa699f53..b082a9a76c33 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.InternalTransaction do use Explorer.Schema - alias Explorer.Chain.{Address, Block, Data, Gas, Hash, PendingBlockOperation, Transaction, Wei} + alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} alias Explorer.Chain.InternalTransaction.{Action, CallType, Result, Type} @typedoc """ @@ -32,50 +32,23 @@ defmodule Explorer.Chain.InternalTransaction do * `block_index` - the index of this internal transaction inside the `block` * `pending_block` - `nil` if `block` has all its internal transactions fetched """ - @type t :: %__MODULE__{ - block_number: Explorer.Chain.Block.block_number() | nil, - type: Type.t(), - call_type: CallType.t() | nil, - created_contract_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - created_contract_address_hash: Hash.t() | nil, - created_contract_code: Data.t() | nil, - error: String.t(), - from_address: %Ecto.Association.NotLoaded{} | Address.t(), - from_address_hash: Hash.Address.t(), - gas: Gas.t() | nil, - gas_used: Gas.t() | nil, - index: non_neg_integer(), - init: Data.t() | nil, - input: Data.t() | nil, - output: Data.t() | nil, - to_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - to_address_hash: Hash.Address.t() | nil, - trace_address: [non_neg_integer()], - transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), - transaction_hash: Hash.t(), - transaction_index: Transaction.transaction_index() | nil, - value: Wei.t(), - block_hash: Hash.Full.t(), - block_index: non_neg_integer() - } - @primary_key false - schema "internal_transactions" do + typed_schema "internal_transactions" do field(:call_type, CallType) field(:created_contract_code, Data) field(:error, :string) field(:gas, :decimal) field(:gas_used, :decimal) - field(:index, :integer, primary_key: true) + field(:index, :integer, primary_key: true, null: false) field(:init, Data) field(:input, Data) field(:output, Data) - field(:trace_address, {:array, :integer}) - field(:type, Type) - field(:value, Wei) + field(:trace_address, {:array, :integer}, null: false) + field(:type, Type, null: false) + field(:value, Wei, null: false) field(:block_number, :integer) field(:transaction_index, :integer) - field(:block_index, :integer) + field(:block_index, :integer, null: false) timestamps() @@ -92,7 +65,8 @@ defmodule Explorer.Chain.InternalTransaction do Address, foreign_key: :from_address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false ) belongs_to( @@ -107,13 +81,15 @@ defmodule Explorer.Chain.InternalTransaction do foreign_key: :transaction_hash, primary_key: true, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) belongs_to(:pending_block, PendingBlockOperation, @@ -580,38 +556,6 @@ defmodule Explorer.Chain.InternalTransaction do ) end - def where_block_number_in_period(query, from_number, to_number) when is_nil(from_number) and not is_nil(to_number) do - where( - query, - [it], - it.block_number <= ^to_number - ) - end - - def where_block_number_in_period(query, from_number, to_number) when not is_nil(from_number) and is_nil(to_number) do - where( - query, - [it], - it.block_number > ^from_number - ) - end - - def where_block_number_in_period(query, from_number, to_number) when is_nil(from_number) and is_nil(to_number) do - where( - query, - [it], - 1 - ) - end - - def where_block_number_in_period(query, from_number, to_number) do - where( - query, - [it], - it.block_number > ^from_number and it.block_number <= ^to_number - ) - end - def where_block_number_is_not_null(query) do where(query, [t], not is_nil(t.block_number)) end diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 3ee92d04240b..183c93230e17 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -7,11 +7,12 @@ defmodule Explorer.Chain.Log do alias ABI.{Event, FunctionSelector} alias Explorer.Chain - alias Explorer.Chain.{Address, Block, ContractMethod, Data, Hash, Transaction} + alias Explorer.Chain.{Address, Block, ContractMethod, Data, Hash, Log, Transaction} + alias Explorer.Chain.SmartContract.Proxy alias Explorer.SmartContract.SigProviderInterface @required_attrs ~w(address_hash data block_hash index transaction_hash)a - @optional_attrs ~w(first_topic second_topic third_topic fourth_topic type block_number)a + @optional_attrs ~w(first_topic second_topic third_topic fourth_topic block_number)a @typedoc """ * `address` - address of contract that generate the event @@ -25,58 +26,41 @@ defmodule Explorer.Chain.Log do * `fourth_topic` - `topics[3]` * `transaction` - transaction for which `log` is * `transaction_hash` - foreign key for `transaction`. - * `index` - index of the log entry in all logs for the `transaction` - * `type` - type of event. *Nethermind-only* + * `index` - index of the log entry within the block """ - @type t :: %__MODULE__{ - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t(), - block_hash: Hash.Full.t(), - block_number: non_neg_integer() | nil, - data: Data.t(), - first_topic: String.t(), - second_topic: String.t(), - third_topic: String.t(), - fourth_topic: String.t(), - transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), - transaction_hash: Hash.Full.t(), - index: non_neg_integer(), - type: String.t() | nil - } - @primary_key false - schema "logs" do - field(:data, Data) - field(:first_topic, :string) - field(:second_topic, :string) - field(:third_topic, :string) - field(:fourth_topic, :string) - field(:index, :integer, primary_key: true) - field(:type, :string) + typed_schema "logs" do + field(:data, Data, null: false) + field(:first_topic, Hash.Full) + field(:second_topic, Hash.Full) + field(:third_topic, Hash.Full) + field(:fourth_topic, Hash.Full) + field(:index, :integer, primary_key: true, null: false) field(:block_number, :integer) timestamps() - belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, primary_key: true, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) belongs_to(:block, Block, foreign_key: :block_hash, primary_key: true, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) end @doc """ - `address_hash` and `transaction_hash` are converted to `t:Explorer.Chain.Hash.t/0`. The allowed values for `type` - are currently unknown, so it is left as a `t:String.t/0`. + `address_hash` and `transaction_hash` are converted to `t:Explorer.Chain.Hash.t/0`. iex> changeset = Explorer.Chain.Log.changeset( ...> %Explorer.Chain.Log{}, @@ -89,8 +73,7 @@ defmodule Explorer.Chain.Log do ...> index: 0, ...> second_topic: nil, ...> third_topic: nil, - ...> transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - ...> type: "mined" + ...> transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" ...> } ...> ) iex> changeset.valid? @@ -106,8 +89,6 @@ defmodule Explorer.Chain.Log do bytes: <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> } - iex> changeset.changes.type - "mined" """ def changeset(%__MODULE__{} = log, attrs \\ %{}) do @@ -120,33 +101,47 @@ defmodule Explorer.Chain.Log do @doc """ Decode transaction log data. """ - + @spec decode(Log.t(), Transaction.t(), any(), boolean, map(), map()) :: + {{:ok, String.t(), String.t(), map()} + | {:error, atom()} + | {:error, atom(), list()} + | {{:error, :contract_not_verified, list()}, any()}, map(), map()} def decode(log, transaction, options, skip_sig_provider?, contracts_acc \\ %{}, events_acc \\ %{}) do - case check_cache(contracts_acc, log.address_hash, options) do - {nil, contracts_acc} -> - {result, events_acc} = find_candidates(log, transaction, options, events_acc) - {result, contracts_acc, events_acc} - - {full_abi, contracts_acc} -> - with {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction), - identifier <- Base.encode16(selector.method_id, case: :lower), - text <- function_call(selector.function, mapping) do - {{:ok, identifier, text, mapping}, contracts_acc, events_acc} - else - {:error, :could_not_decode} -> - case find_candidates(log, transaction, options, events_acc) do - {{:error, :contract_not_verified, []}, events_acc} -> - {decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc} + with {full_abi, contracts_acc} <- check_cache(contracts_acc, log.address_hash, options), + {:no_abi, false} <- {:no_abi, is_nil(full_abi)}, + {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction.hash), + identifier <- Base.encode16(selector.method_id, case: :lower), + text <- function_call(selector.function, mapping) do + {{:ok, identifier, text, mapping}, contracts_acc, events_acc} + else + {:error, _} = error -> + handle_method_decode_error(error, log, transaction, options, skip_sig_provider?, contracts_acc, events_acc) + + {:no_abi, true} -> + handle_method_decode_error( + {:error, :could_not_decode}, + log, + transaction, + options, + skip_sig_provider?, + contracts_acc, + events_acc + ) + end + end - {{:error, :contract_not_verified, candidates}, events_acc} -> - {{:error, :contract_verified, candidates}, contracts_acc, events_acc} + defp handle_method_decode_error(error, log, transaction, options, skip_sig_provider?, contracts_acc, events_acc) do + case error do + {:error, _reason} -> + case find_method_candidates(log, transaction, options, events_acc, skip_sig_provider?) do + {{:error, :contract_not_verified, []}, events_acc} -> + {decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc} - {_, events_acc} -> - {decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc} - end + {{:error, :contract_not_verified, candidates}, events_acc} -> + {{:error, :contract_not_verified, candidates}, contracts_acc, events_acc} - {:error, reason} -> - {{:error, reason}, contracts_acc, events_acc} + {_, events_acc} -> + {decode_event_via_sig_provider(log, transaction, false, skip_sig_provider?), contracts_acc, events_acc} end end end @@ -165,7 +160,7 @@ defmodule Explorer.Chain.Log do else case Chain.find_contract_address(address_hash, address_options, false) do {:ok, %{smart_contract: smart_contract}} -> - full_abi = Chain.combine_proxy_implementation_abi(smart_contract, options) + full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, options) {full_abi, Map.put(acc, address_hash, full_abi)} _ -> @@ -174,36 +169,30 @@ defmodule Explorer.Chain.Log do end end - defp find_candidates(log, transaction, options, events_acc) do - case log.first_topic do - "0x" <> hex_part -> - case Integer.parse(hex_part, 16) do - {number, ""} -> - <> = :binary.encode_unsigned(number) - check_events_cache(events_acc, method_id, log, transaction, options) - - _ -> - {{:error, :could_not_decode}, events_acc} - end + defp find_method_candidates(log, transaction, options, events_acc, skip_sig_provider?) do + if is_nil(log.first_topic) do + {{:error, :could_not_decode}, events_acc} + else + <> = log.first_topic.bytes + key = {method_id, log.second_topic, log.third_topic, log.fourth_topic} - _ -> - {{:error, :could_not_decode}, events_acc} + if Map.has_key?(events_acc, key) do + {events_acc[key], events_acc} + else + result = find_method_candidates_from_db(method_id, log, transaction, options, skip_sig_provider?) + {result, Map.put(events_acc, key, result)} + end end end - defp find_candidates_query(method_id, log, transaction, options) do - candidates_query = - from( - contract_method in ContractMethod, - where: contract_method.identifier == ^method_id, - limit: 3 - ) + defp find_method_candidates_from_db(method_id, log, transaction, options, skip_sig_provider?) do + candidates_query = ContractMethod.find_contract_method_query(method_id, 3) candidates = candidates_query |> Chain.select_repo(options).all() |> Enum.flat_map(fn contract_method -> - case find_and_decode([contract_method.abi], log, transaction) do + case find_and_decode([contract_method.abi], log, transaction.hash) do {:ok, selector, mapping} -> identifier = Base.encode16(selector.method_id, case: :lower) text = function_call(selector.function, mapping) @@ -217,39 +206,41 @@ defmodule Explorer.Chain.Log do |> Enum.take(1) {:error, :contract_not_verified, - if(candidates == [], do: decode_event_via_sig_provider(log, transaction, true), else: candidates)} + if(candidates == [], + do: + if(skip_sig_provider?, + do: [], + else: decode_event_via_sig_provider(log, transaction, true) + ), + else: candidates + )} end - defp check_events_cache(events_acc, method_id, log, transaction, options) do - if Map.has_key?(events_acc, method_id) do - {events_acc[method_id], events_acc} - else - result = find_candidates_query(method_id, log, transaction, options) - {result, Map.put(events_acc, method_id, result)} - end - end - - @spec find_and_decode([map()], __MODULE__.t(), Transaction.t()) :: + @spec find_and_decode([map()], __MODULE__.t(), Hash.t()) :: {:error, any} | {:ok, ABI.FunctionSelector.t(), any} - def find_and_decode(abi, log, transaction) do - with {%FunctionSelector{} = selector, mapping} <- + def find_and_decode(abi, log, transaction_hash) do + # For events, the method_id (signature) is 32 bytes, whereas for methods and + # errors it is 4 bytes. To avoid complications with different sizes, we + # always take only the first 4 bytes of the hash. + with {%FunctionSelector{method_id: <>} = selector, mapping} <- abi |> ABI.parse_specification(include_events?: true) |> Event.find_and_decode( - decode16!(log.first_topic), - decode16!(log.second_topic), - decode16!(log.third_topic), - decode16!(log.fourth_topic), + log.first_topic && log.first_topic.bytes, + log.second_topic && log.second_topic.bytes, + log.third_topic && log.third_topic.bytes, + log.fourth_topic && log.fourth_topic.bytes, log.data.bytes - ) do + ), + selector <- %{selector | method_id: first_four_bytes} do {:ok, selector, mapping} end rescue e -> Logger.warn(fn -> [ - "Could not decode input data for log from transaction: ", - Hash.to_iodata(transaction.hash), + "Could not decode input data for log from transaction hash: ", + Hash.to_iodata(transaction_hash), Exception.format(:error, e, __STACKTRACE__) ] end) @@ -261,12 +252,7 @@ defmodule Explorer.Chain.Log do text = mapping |> Stream.map(fn {name, type, indexed?, _value} -> - indexed_keyword = - if indexed? do - ["indexed "] - else - [] - end + indexed_keyword = if indexed?, do: ["indexed "], else: [] [type, " ", indexed_keyword, name] end) @@ -275,7 +261,12 @@ defmodule Explorer.Chain.Log do IO.iodata_to_binary([name, "(", text, ")"]) end - defp decode_event_via_sig_provider(log, transaction, only_candidates?, skip_sig_provider? \\ false) do + defp decode_event_via_sig_provider( + log, + transaction, + only_candidates?, + skip_sig_provider? \\ false + ) do with true <- SigProviderInterface.enabled?(), false <- skip_sig_provider?, {:ok, result} <- @@ -291,7 +282,7 @@ defmodule Explorer.Chain.Log do true <- is_list(result), false <- Enum.empty?(result), abi <- [result |> List.first() |> Map.put("type", "event")], - {:ok, selector, mapping} <- find_and_decode(abi, log, transaction), + {:ok, selector, mapping} <- find_and_decode(abi, log, transaction.hash), identifier <- Base.encode16(selector.method_id, case: :lower), text <- function_call(selector.function, mapping) do if only_candidates? do @@ -323,4 +314,21 @@ defmodule Explorer.Chain.Log do |> limit(1) |> Chain.select_repo(options).one() end + + @doc """ + Fetches logs by user operation. + """ + @spec user_op_to_logs(map(), Keyword.t()) :: [t()] + def user_op_to_logs(user_op, options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + limit = Keyword.get(options, :limit, 50) + + __MODULE__ + |> where([log], log.block_hash == ^user_op["block_hash"] and log.transaction_hash == ^user_op["transaction_hash"]) + |> where([log], log.index >= ^user_op["user_logs_start_index"]) + |> order_by([log], asc: log.index) + |> limit(^min(user_op["user_logs_count"], limit)) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end end diff --git a/apps/explorer/lib/explorer/chain/null_round_height.ex b/apps/explorer/lib/explorer/chain/null_round_height.ex new file mode 100644 index 000000000000..41d0fb19049c --- /dev/null +++ b/apps/explorer/lib/explorer/chain/null_round_height.ex @@ -0,0 +1,81 @@ +defmodule Explorer.Chain.NullRoundHeight do + @moduledoc """ + A null round is formed when a block at height N links to a block at height N-2 instead of N-1 + """ + + use Explorer.Schema + + alias Explorer.Repo + + @primary_key false + schema "null_round_heights" do + field(:height, :integer, primary_key: true) + end + + def changeset(null_round_height \\ %__MODULE__{}, params) do + null_round_height + |> cast(params, [:height]) + |> validate_required([:height]) + |> unique_constraint(:height) + end + + def total do + Repo.aggregate(__MODULE__, :count) + end + + def insert_heights(heights) do + params = + heights + |> Enum.uniq() + |> Enum.map(&%{height: &1}) + + Repo.insert_all(__MODULE__, params, on_conflict: :nothing) + end + + defp find_neighbor_from_previous(previous_null_rounds, number, direction) do + previous_null_rounds + |> Enum.reduce_while({number, nil}, fn height, {current, _result} -> + if height == move_by_one(current, direction) do + {:cont, {height, nil}} + else + {:halt, {nil, move_by_one(current, direction)}} + end + end) + |> elem(1) + |> case do + nil -> + previous_null_rounds + |> List.last() + |> neighbor_block_number(direction) + + number -> + number + end + end + + def neighbor_block_number(number, direction) do + number + |> neighbors_query(direction) + |> select([nrh], nrh.height) + |> Repo.all() + |> case do + [] -> + move_by_one(number, direction) + + previous_null_rounds -> + find_neighbor_from_previous(previous_null_rounds, number, direction) + end + end + + defp move_by_one(number, :previous), do: number - 1 + defp move_by_one(number, :next), do: number + 1 + + @batch_size 5 + defp neighbors_query(number, :previous) do + from(nrh in __MODULE__, where: nrh.height < ^number, order_by: [desc: :height], limit: @batch_size) + end + + defp neighbors_query(number, :next) do + from(nrh in __MODULE__, where: nrh.height > ^number, order_by: [asc: :height], limit: @batch_size) + end +end diff --git a/apps/explorer/lib/explorer/chain/optimism/deposit.ex b/apps/explorer/lib/explorer/chain/optimism/deposit.ex new file mode 100644 index 000000000000..7bf87b619886 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/optimism/deposit.ex @@ -0,0 +1,86 @@ +defmodule Explorer.Chain.Optimism.Deposit do + @moduledoc "Models a deposit for Optimism." + + use Explorer.Schema + + import Explorer.Chain, only: [join_association: 3, select_repo: 1] + + alias Explorer.Chain.{Hash, Transaction} + alias Explorer.PagingOptions + + @default_paging_options %PagingOptions{page_size: 50} + + @required_attrs ~w(l1_block_number l1_transaction_hash l1_transaction_origin l2_transaction_hash)a + @optional_attrs ~w(l1_block_timestamp)a + @allowed_attrs @required_attrs ++ @optional_attrs + + @type t :: %__MODULE__{ + l1_block_number: non_neg_integer(), + l1_block_timestamp: DateTime.t(), + l1_transaction_hash: Hash.t(), + l1_transaction_origin: Hash.t(), + l2_transaction_hash: Hash.t(), + l2_transaction: %Ecto.Association.NotLoaded{} | Transaction.t() + } + + @primary_key false + schema "op_deposits" do + field(:l1_block_number, :integer) + field(:l1_block_timestamp, :utc_datetime_usec) + field(:l1_transaction_hash, Hash.Full) + field(:l1_transaction_origin, Hash.Address) + + belongs_to(:l2_transaction, Transaction, + foreign_key: :l2_transaction_hash, + primary_key: true, + references: :hash, + type: Hash.Full + ) + + timestamps() + end + + def changeset(%__MODULE__{} = deposit, attrs \\ %{}) do + deposit + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:l2_transaction_hash) + end + + def last_deposit_l1_block_number_query do + from(d in __MODULE__, + select: {d.l1_block_number, d.l1_transaction_hash}, + order_by: [desc: d.l1_block_number], + limit: 1 + ) + end + + @doc """ + Lists `t:Explorer.Chain.Optimism.Deposit.t/0`'s' in descending order based on l1_block_number and l2_transaction_hash. + + """ + @spec list :: [__MODULE__.t()] + def list(options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + base_query = + from(d in __MODULE__, + order_by: [desc: d.l1_block_number, desc: d.l2_transaction_hash] + ) + + base_query + |> join_association(:l2_transaction, :required) + |> page_deposits(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + defp page_deposits(query, %PagingOptions{key: nil}), do: query + + defp page_deposits(query, %PagingOptions{key: {block_number, l2_tx_hash}}) do + from(d in query, + where: d.l1_block_number < ^block_number, + or_where: d.l1_block_number == ^block_number and d.l2_transaction_hash < ^l2_tx_hash + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex new file mode 100644 index 000000000000..49aceda7a4f1 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex @@ -0,0 +1,33 @@ +defmodule Explorer.Chain.Optimism.FrameSequence do + @moduledoc "Models a frame sequence for Optimism." + + use Explorer.Schema + + alias Explorer.Chain.Hash + alias Explorer.Chain.Optimism.TxnBatch + + @required_attrs ~w(id l1_transaction_hashes l1_timestamp)a + + @type t :: %__MODULE__{ + l1_transaction_hashes: [Hash.t()], + l1_timestamp: DateTime.t(), + transaction_batches: %Ecto.Association.NotLoaded{} | [TxnBatch.t()] + } + + @primary_key {:id, :integer, autogenerate: false} + schema "op_frame_sequences" do + field(:l1_transaction_hashes, {:array, Hash.Full}) + field(:l1_timestamp, :utc_datetime_usec) + + has_many(:transaction_batches, TxnBatch, foreign_key: :frame_sequence_id) + + timestamps() + end + + def changeset(%__MODULE__{} = sequences, attrs \\ %{}) do + sequences + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:id) + end +end diff --git a/apps/explorer/lib/explorer/chain/optimism/output_root.ex b/apps/explorer/lib/explorer/chain/optimism/output_root.ex new file mode 100644 index 000000000000..99f69edb251e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/optimism/output_root.ex @@ -0,0 +1,67 @@ +defmodule Explorer.Chain.Optimism.OutputRoot do + @moduledoc "Models an output root for Optimism." + + use Explorer.Schema + + import Explorer.Chain, only: [select_repo: 1] + + alias Explorer.Chain.Hash + alias Explorer.PagingOptions + + @default_paging_options %PagingOptions{page_size: 50} + + @required_attrs ~w(l2_output_index l2_block_number l1_transaction_hash l1_timestamp l1_block_number output_root)a + + @type t :: %__MODULE__{ + l2_output_index: non_neg_integer(), + l2_block_number: non_neg_integer(), + l1_transaction_hash: Hash.t(), + l1_timestamp: DateTime.t(), + l1_block_number: non_neg_integer(), + output_root: Hash.t() + } + + @primary_key false + schema "op_output_roots" do + field(:l2_output_index, :integer, primary_key: true) + field(:l2_block_number, :integer) + field(:l1_transaction_hash, Hash.Full) + field(:l1_timestamp, :utc_datetime_usec) + field(:l1_block_number, :integer) + field(:output_root, Hash.Full) + + timestamps() + end + + def changeset(%__MODULE__{} = output_roots, attrs \\ %{}) do + output_roots + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + end + + @doc """ + Lists `t:Explorer.Chain.Optimism.OutputRoot.t/0`'s' in descending order based on output root index. + + """ + @spec list :: [__MODULE__.t()] + def list(options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + base_query = + from(r in __MODULE__, + order_by: [desc: r.l2_output_index], + select: r + ) + + base_query + |> page_output_roots(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + defp page_output_roots(query, %PagingOptions{key: nil}), do: query + + defp page_output_roots(query, %PagingOptions{key: {index}}) do + from(r in query, where: r.l2_output_index < ^index) + end +end diff --git a/apps/explorer/lib/explorer/chain/optimism/txn_batch.ex b/apps/explorer/lib/explorer/chain/optimism/txn_batch.ex new file mode 100644 index 000000000000..83b70e3d24fa --- /dev/null +++ b/apps/explorer/lib/explorer/chain/optimism/txn_batch.ex @@ -0,0 +1,156 @@ +defmodule Explorer.Chain.Optimism.TxnBatch do + @moduledoc "Models a batch of transactions for Optimism." + + use Explorer.Schema + + import Explorer.Chain, only: [join_association: 3, select_repo: 1] + + alias Explorer.Chain.Optimism.FrameSequence + alias Explorer.PagingOptions + + @default_paging_options %PagingOptions{page_size: 50} + + @required_attrs ~w(l2_block_number frame_sequence_id)a + + @blob_size 4096 * 32 + @encoding_version 0 + @max_blob_data_size (4 * 31 + 3) * 1024 - 4 + @rounds 1024 + + @type t :: %__MODULE__{ + l2_block_number: non_neg_integer(), + frame_sequence_id: non_neg_integer(), + frame_sequence: %Ecto.Association.NotLoaded{} | FrameSequence.t() + } + + @primary_key false + schema "op_transaction_batches" do + field(:l2_block_number, :integer, primary_key: true) + belongs_to(:frame_sequence, FrameSequence, foreign_key: :frame_sequence_id, references: :id, type: :integer) + + timestamps() + end + + def changeset(%__MODULE__{} = batches, attrs \\ %{}) do + batches + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:frame_sequence_id) + end + + @doc """ + Lists `t:Explorer.Chain.Optimism.TxnBatch.t/0`'s' in descending order based on l2_block_number. + + """ + @spec list :: [__MODULE__.t()] + def list(options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + base_query = + from(tb in __MODULE__, + order_by: [desc: tb.l2_block_number] + ) + + base_query + |> join_association(:frame_sequence, :required) + |> page_txn_batches(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + @doc """ + Decodes EIP-4844 blob to the raw data. Returns `nil` if the blob is invalid. + """ + @spec decode_eip4844_blob(binary()) :: binary() | nil + def decode_eip4844_blob(b) do + <> = b + + if version != @encoding_version or output_len > @max_blob_data_size do + raise "Blob version or data size is incorrect" + end + + output = first_output <> :binary.copy(<<0>>, @max_blob_data_size - 27) + + opos = 28 + ipos = 32 + {encoded_byte1, opos, ipos, output} = decode_eip4844_field_element(b, opos, ipos, output) + {encoded_byte2, opos, ipos, output} = decode_eip4844_field_element(b, opos, ipos, output) + {encoded_byte3, opos, ipos, output} = decode_eip4844_field_element(b, opos, ipos, output) + {opos, output} = reassemble_eip4844_bytes(opos, encoded_byte0, encoded_byte1, encoded_byte2, encoded_byte3, output) + + {_opos, ipos, output} = + Enum.reduce_while(Range.new(1, @rounds - 1), {opos, ipos, output}, fn _i, {opos_acc, ipos_acc, output_acc} -> + if opos_acc >= output_len do + {:halt, {opos_acc, ipos_acc, output_acc}} + else + {encoded_byte0, opos_acc, ipos_acc, output_acc} = + decode_eip4844_field_element(b, opos_acc, ipos_acc, output_acc) + + {encoded_byte1, opos_acc, ipos_acc, output_acc} = + decode_eip4844_field_element(b, opos_acc, ipos_acc, output_acc) + + {encoded_byte2, opos_acc, ipos_acc, output_acc} = + decode_eip4844_field_element(b, opos_acc, ipos_acc, output_acc) + + {encoded_byte3, opos_acc, ipos_acc, output_acc} = + decode_eip4844_field_element(b, opos_acc, ipos_acc, output_acc) + + {opos_acc, output_acc} = + reassemble_eip4844_bytes(opos_acc, encoded_byte0, encoded_byte1, encoded_byte2, encoded_byte3, output_acc) + + {:cont, {opos_acc, ipos_acc, output_acc}} + end + end) + + Enum.each(Range.new(output_len, byte_size(output) - 1, 1), fn i -> + <<0>> = binary_part(output, i, 1) + end) + + output = binary_part(output, 0, output_len) + + Enum.each(Range.new(ipos, @blob_size - 1, 1), fn i -> + <<0>> = binary_part(b, i, 1) + end) + + output + rescue + _ -> nil + end + + defp decode_eip4844_field_element(b, opos, ipos, output) do + <<_::binary-size(ipos), ipos_byte::size(8), insert::binary-size(31), _::binary>> = b + + if Bitwise.band(ipos_byte, 0b11000000) == 0 do + <> = output + + {ipos_byte, opos + 32, ipos + 32, output_before_opos <> insert <> rest} + end + end + + defp reassemble_eip4844_bytes(opos, encoded_byte0, encoded_byte1, encoded_byte2, encoded_byte3, output) do + opos = opos - 1 + + x = Bitwise.bor(Bitwise.band(encoded_byte0, 0b00111111), Bitwise.bsl(Bitwise.band(encoded_byte1, 0b00110000), 2)) + y = Bitwise.bor(Bitwise.band(encoded_byte1, 0b00001111), Bitwise.bsl(Bitwise.band(encoded_byte3, 0b00001111), 4)) + z = Bitwise.bor(Bitwise.band(encoded_byte2, 0b00111111), Bitwise.bsl(Bitwise.band(encoded_byte3, 0b00110000), 2)) + + new_output = + output + |> replace_byte(z, opos - 32) + |> replace_byte(y, opos - 32 * 2) + |> replace_byte(x, opos - 32 * 3) + + {opos, new_output} + end + + defp replace_byte(bytes, byte, pos) do + <> = bytes + bytes_before <> <> <> bytes_after + end + + defp page_txn_batches(query, %PagingOptions{key: nil}), do: query + + defp page_txn_batches(query, %PagingOptions{key: {block_number}}) do + from(tb in query, where: tb.l2_block_number < ^block_number) + end +end diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex new file mode 100644 index 000000000000..af7c062a47cd --- /dev/null +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex @@ -0,0 +1,163 @@ +defmodule Explorer.Chain.Optimism.Withdrawal do + @moduledoc "Models Optimism withdrawal." + + use Explorer.Schema + + import Explorer.Chain, only: [select_repo: 1] + + alias Explorer.Chain.{Block, Hash, Transaction} + alias Explorer.Chain.Cache.OptimismFinalizationPeriod + alias Explorer.Chain.Optimism.{OutputRoot, WithdrawalEvent} + alias Explorer.{PagingOptions, Repo} + + @default_paging_options %PagingOptions{page_size: 50} + + @required_attrs ~w(msg_nonce hash l2_transaction_hash l2_block_number)a + + @type t :: %__MODULE__{ + msg_nonce: Decimal.t(), + hash: Hash.t(), + l2_transaction_hash: Hash.t(), + l2_block_number: non_neg_integer() + } + + @primary_key false + schema "op_withdrawals" do + field(:msg_nonce, :decimal, primary_key: true) + field(:hash, Hash.Full) + field(:l2_transaction_hash, Hash.Full) + field(:l2_block_number, :integer) + + timestamps() + end + + def changeset(%__MODULE__{} = withdrawals, attrs \\ %{}) do + withdrawals + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + end + + @doc """ + Lists `t:Explorer.Chain.Optimism.Withdrawal.t/0`'s' in descending order based on message nonce. + + """ + @spec list :: [__MODULE__.t()] + def list(options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + base_query = + from(w in __MODULE__, + order_by: [desc: w.msg_nonce], + left_join: l2_tx in Transaction, + on: w.l2_transaction_hash == l2_tx.hash, + left_join: l2_block in Block, + on: w.l2_block_number == l2_block.number, + left_join: we in WithdrawalEvent, + on: we.withdrawal_hash == w.hash and we.l1_event_type == :WithdrawalFinalized, + select: %{ + msg_nonce: w.msg_nonce, + hash: w.hash, + l2_block_number: w.l2_block_number, + l2_timestamp: l2_block.timestamp, + l2_transaction_hash: w.l2_transaction_hash, + l1_transaction_hash: we.l1_transaction_hash, + from: l2_tx.from_address_hash + } + ) + + base_query + |> page_optimism_withdrawals(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + defp page_optimism_withdrawals(query, %PagingOptions{key: nil}), do: query + + defp page_optimism_withdrawals(query, %PagingOptions{key: {nonce}}) do + from(w in query, where: w.msg_nonce < ^nonce) + end + + @doc """ + Gets withdrawal statuses for Optimism Withdrawal transaction. + For each withdrawal associated with this transaction, + returns the status and the corresponding L1 transaction hash if the status is `Relayed`. + """ + @spec transaction_statuses(Hash.t()) :: [{non_neg_integer(), String.t(), Hash.t() | nil}] + def transaction_statuses(l2_transaction_hash) do + query = + from(w in __MODULE__, + where: w.l2_transaction_hash == ^l2_transaction_hash, + left_join: l2_block in Block, + on: w.l2_block_number == l2_block.number and l2_block.consensus == true, + left_join: we in WithdrawalEvent, + on: we.withdrawal_hash == w.hash and we.l1_event_type == :WithdrawalFinalized, + select: %{ + hash: w.hash, + l2_block_number: w.l2_block_number, + l1_transaction_hash: we.l1_transaction_hash, + msg_nonce: w.msg_nonce + } + ) + + query + |> Repo.replica().all() + |> Enum.map(fn w -> + msg_nonce = + Bitwise.band( + Decimal.to_integer(w.msg_nonce), + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ) + + {status, _} = status(w) + {msg_nonce, status, w.l1_transaction_hash} + end) + end + + @doc """ + Gets Optimism Withdrawal status and remaining time to unlock (when the status is `In challenge period`). + """ + @spec status(map()) :: {String.t(), DateTime.t() | nil} + def status(w) when is_nil(w.l1_transaction_hash) do + l1_timestamp = + Repo.replica().one( + from( + we in WithdrawalEvent, + select: we.l1_timestamp, + where: we.withdrawal_hash == ^w.hash and we.l1_event_type == :WithdrawalProven + ) + ) + + if is_nil(l1_timestamp) do + last_root_l2_block_number = + Repo.replica().one( + from(root in OutputRoot, + select: root.l2_block_number, + order_by: [desc: root.l2_output_index], + limit: 1 + ) + ) || 0 + + if w.l2_block_number > last_root_l2_block_number do + {"Waiting for state root", nil} + else + {"Ready to prove", nil} + end + else + challenge_period = + case OptimismFinalizationPeriod.get_period() do + nil -> 604_800 + period -> period + end + + if DateTime.compare(l1_timestamp, DateTime.add(DateTime.utc_now(), -challenge_period, :second)) == :lt do + {"Ready for relay", nil} + else + {"In challenge period", DateTime.add(l1_timestamp, challenge_period, :second)} + end + end + end + + def status(_w) do + {"Relayed", nil} + end +end diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex new file mode 100644 index 000000000000..3cdecc12cb93 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex @@ -0,0 +1,34 @@ +defmodule Explorer.Chain.Optimism.WithdrawalEvent do + @moduledoc "Models Optimism withdrawal event." + + use Explorer.Schema + + alias Explorer.Chain.Hash + + @required_attrs ~w(withdrawal_hash l1_event_type l1_timestamp l1_transaction_hash l1_block_number)a + + @type t :: %__MODULE__{ + withdrawal_hash: Hash.t(), + l1_event_type: String.t(), + l1_timestamp: DateTime.t(), + l1_transaction_hash: Hash.t(), + l1_block_number: non_neg_integer() + } + + @primary_key false + schema "op_withdrawal_events" do + field(:withdrawal_hash, Hash.Full, primary_key: true) + field(:l1_event_type, Ecto.Enum, values: [:WithdrawalProven, :WithdrawalFinalized], primary_key: true) + field(:l1_timestamp, :utc_datetime_usec) + field(:l1_transaction_hash, Hash.Full) + field(:l1_block_number, :integer) + + timestamps() + end + + def changeset(%__MODULE__{} = withdrawal_events, attrs \\ %{}) do + withdrawal_events + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + end +end diff --git a/apps/explorer/lib/explorer/chain/pending_block_operation.ex b/apps/explorer/lib/explorer/chain/pending_block_operation.ex index 8c4a99a5b4e1..0852e317b8d3 100644 --- a/apps/explorer/lib/explorer/chain/pending_block_operation.ex +++ b/apps/explorer/lib/explorer/chain/pending_block_operation.ex @@ -12,17 +12,19 @@ defmodule Explorer.Chain.PendingBlockOperation do @typedoc """ * `block_hash` - the hash of the block that has pending operations. """ - @type t :: %__MODULE__{ - block_hash: Hash.Full.t() - } - @primary_key false - schema "pending_block_operations" do + typed_schema "pending_block_operations" do timestamps() - field(:block_number, :integer) + field(:block_number, :integer, null: false) - belongs_to(:block, Block, foreign_key: :block_hash, primary_key: true, references: :hash, type: Hash.Full) + belongs_to(:block, Block, + foreign_key: :block_hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) end def changeset(%__MODULE__{} = pending_ops, attrs) do diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex b/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex index b9ad75bc3a5b..a18900868876 100644 --- a/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex +++ b/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex @@ -22,23 +22,14 @@ defmodule Explorer.Chain.PolygonEdge.Deposit do * `l1_timestamp` - timestamp of the L1 transaction block * `l1_block_number` - block number of the L1 transaction """ - @type t :: %__MODULE__{ - msg_id: non_neg_integer(), - from: Hash.Address.t() | nil, - to: Hash.Address.t() | nil, - l1_transaction_hash: Hash.t() | nil, - l1_timestamp: DateTime.t() | nil, - l1_block_number: Block.block_number() - } - @primary_key false - schema "polygon_edge_deposits" do - field(:msg_id, :integer, primary_key: true) + typed_schema "polygon_edge_deposits" do + field(:msg_id, :integer, primary_key: true, null: false) field(:from, Hash.Address) field(:to, Hash.Address) field(:l1_transaction_hash, Hash.Full) field(:l1_timestamp, :utc_datetime_usec) - field(:l1_block_number, :integer) + field(:l1_block_number, :integer) :: Block.block_number() timestamps() end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex b/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex index e3e7617d579f..7e2ed2c64e5e 100644 --- a/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex +++ b/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.PolygonEdge.DepositExecute do use Explorer.Schema - alias Explorer.Chain.{Block, Hash} + alias Explorer.Chain.Hash @required_attrs ~w(msg_id l2_transaction_hash l2_block_number success)a @@ -13,19 +13,12 @@ defmodule Explorer.Chain.PolygonEdge.DepositExecute do * `l2_block_number` - block number of the L2 transaction * `success` - a status of onStateReceive internal call (namely internal deposit transaction) """ - @type t :: %__MODULE__{ - msg_id: non_neg_integer(), - l2_transaction_hash: Hash.t(), - l2_block_number: Block.block_number(), - success: boolean() - } - @primary_key false - schema "polygon_edge_deposit_executes" do - field(:msg_id, :integer, primary_key: true) - field(:l2_transaction_hash, Hash.Full) - field(:l2_block_number, :integer) - field(:success, :boolean) + typed_schema "polygon_edge_deposit_executes" do + field(:msg_id, :integer, primary_key: true, null: false) + field(:l2_transaction_hash, Hash.Full, null: false) + field(:l2_block_number, :integer, null: false) + field(:success, :boolean, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex b/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex index 9cfc109b0bcb..22b50b1ffa13 100644 --- a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex @@ -23,26 +23,21 @@ defmodule Explorer.Chain.PolygonEdge.Withdrawal do * `l2_transaction_hash` - hash of the L2 transaction containing the corresponding L2StateSynced event * `l2_block_number` - block number of the L2 transaction """ - @type t :: %__MODULE__{ - msg_id: non_neg_integer(), - from: Hash.Address.t() | nil, - from_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - to: Hash.Address.t() | nil, - to_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - l2_transaction_hash: Hash.t(), - l2_transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), - l2_block_number: Block.block_number(), - l2_block: %Ecto.Association.NotLoaded{} | Block.t() - } - @primary_key false - schema "polygon_edge_withdrawals" do - field(:msg_id, :integer, primary_key: true) + typed_schema "polygon_edge_withdrawals" do + field(:msg_id, :integer, primary_key: true, null: false) belongs_to(:from_address, Address, foreign_key: :from, references: :hash, type: Hash.Address) belongs_to(:to_address, Address, foreign_key: :to, references: :hash, type: Hash.Address) - belongs_to(:l2_transaction, Transaction, foreign_key: :l2_transaction_hash, references: :hash, type: Hash.Full) - belongs_to(:l2_block, Block, foreign_key: :l2_block_number, references: :number, type: :integer) + + belongs_to(:l2_transaction, Transaction, + foreign_key: :l2_transaction_hash, + references: :hash, + type: Hash.Full, + null: false + ) + + belongs_to(:l2_block, Block, foreign_key: :l2_block_number, references: :number, type: :integer, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex b/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex index 27ee5583af66..959d3bfc68a3 100644 --- a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex +++ b/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex @@ -13,19 +13,12 @@ defmodule Explorer.Chain.PolygonEdge.WithdrawalExit do * `l1_block_number` - block number of the L1 transaction * `success` - a status of onL2StateReceive internal call (namely internal withdrawal transaction) """ - @type t :: %__MODULE__{ - msg_id: non_neg_integer(), - l1_transaction_hash: Hash.t(), - l1_block_number: Block.block_number(), - success: boolean() - } - @primary_key false - schema "polygon_edge_withdrawal_exits" do - field(:msg_id, :integer, primary_key: true) - field(:l1_transaction_hash, Hash.Full) - field(:l1_block_number, :integer) - field(:success, :boolean) + typed_schema "polygon_edge_withdrawal_exits" do + field(:msg_id, :integer, primary_key: true, null: false) + field(:l1_transaction_hash, Hash.Full, null: false) + field(:l1_block_number, :integer) :: Block.block_number() + field(:success, :boolean, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/polygon_zkevm/batch_transaction.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/batch_transaction.ex new file mode 100644 index 000000000000..18d8a775a1b3 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/batch_transaction.ex @@ -0,0 +1,37 @@ +defmodule Explorer.Chain.PolygonZkevm.BatchTransaction do + @moduledoc "Models a list of transactions related to a batch for zkEVM." + + use Explorer.Schema + + alias Explorer.Chain.{Hash, Transaction} + alias Explorer.Chain.PolygonZkevm.TransactionBatch + + @required_attrs ~w(batch_number hash)a + + @primary_key false + typed_schema "polygon_zkevm_batch_l2_transactions" do + belongs_to(:batch, TransactionBatch, foreign_key: :batch_number, references: :number, type: :integer, null: false) + + belongs_to(:l2_transaction, Transaction, + foreign_key: :hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) + + timestamps(null: false) + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = transactions, attrs \\ %{}) do + transactions + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:batch_number) + |> unique_constraint(:hash) + end +end diff --git a/apps/explorer/lib/explorer/chain/polygon_zkevm/bridge.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/bridge.ex new file mode 100644 index 000000000000..c0623f8fbf52 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/bridge.ex @@ -0,0 +1,55 @@ +defmodule Explorer.Chain.PolygonZkevm.Bridge do + @moduledoc "Models a bridge operation for Polygon zkEVM." + + use Explorer.Schema + + alias Explorer.Chain.{Block, Hash, Token} + alias Explorer.Chain.PolygonZkevm.BridgeL1Token + + @optional_attrs ~w(l1_transaction_hash l2_transaction_hash l1_token_id l2_token_address block_number block_timestamp)a + + @required_attrs ~w(type index amount)a + + @type t :: %__MODULE__{ + type: String.t(), + index: non_neg_integer(), + l1_transaction_hash: Hash.t() | nil, + l2_transaction_hash: Hash.t() | nil, + l1_token: %Ecto.Association.NotLoaded{} | BridgeL1Token.t() | nil, + l1_token_id: non_neg_integer() | nil, + l1_token_address: Hash.Address.t() | nil, + l2_token: %Ecto.Association.NotLoaded{} | Token.t() | nil, + l2_token_address: Hash.Address.t() | nil, + amount: Decimal.t(), + block_number: Block.block_number() | nil, + block_timestamp: DateTime.t() | nil + } + + @primary_key false + schema "polygon_zkevm_bridge" do + field(:type, Ecto.Enum, values: [:deposit, :withdrawal], primary_key: true) + field(:index, :integer, primary_key: true) + field(:l1_transaction_hash, Hash.Full) + field(:l2_transaction_hash, Hash.Full) + belongs_to(:l1_token, BridgeL1Token, foreign_key: :l1_token_id, references: :id, type: :integer) + field(:l1_token_address, Hash.Address) + belongs_to(:l2_token, Token, foreign_key: :l2_token_address, references: :contract_address_hash, type: Hash.Address) + field(:amount, :decimal) + field(:block_number, :integer) + field(:block_timestamp, :utc_datetime_usec) + + timestamps() + end + + @doc """ + Checks that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = operations, attrs \\ %{}) do + operations + |> cast(attrs, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> unique_constraint([:type, :index]) + |> foreign_key_constraint(:l1_token_id) + end +end diff --git a/apps/explorer/lib/explorer/chain/polygon_zkevm/bridge_l1_token.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/bridge_l1_token.ex new file mode 100644 index 000000000000..c3187c28ea40 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/bridge_l1_token.ex @@ -0,0 +1,37 @@ +defmodule Explorer.Chain.PolygonZkevm.BridgeL1Token do + @moduledoc "Models a bridge token on L1 for Polygon zkEVM." + + use Explorer.Schema + + alias Explorer.Chain.Hash + + @optional_attrs ~w(decimals symbol)a + + @required_attrs ~w(address)a + + @type t :: %__MODULE__{ + address: Hash.Address.t(), + decimals: non_neg_integer() | nil, + symbol: String.t() | nil + } + + @primary_key {:id, :id, autogenerate: true} + schema "polygon_zkevm_bridge_l1_tokens" do + field(:address, Hash.Address) + field(:decimals, :integer) + field(:symbol, :string) + + timestamps() + end + + @doc """ + Checks that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = tokens, attrs \\ %{}) do + tokens + |> cast(attrs, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:id) + end +end diff --git a/apps/explorer/lib/explorer/chain/zkevm/lifecycle_transaction.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/lifecycle_transaction.ex similarity index 60% rename from apps/explorer/lib/explorer/chain/zkevm/lifecycle_transaction.ex rename to apps/explorer/lib/explorer/chain/polygon_zkevm/lifecycle_transaction.ex index 480b0c0c80fa..504cdce41663 100644 --- a/apps/explorer/lib/explorer/chain/zkevm/lifecycle_transaction.ex +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/lifecycle_transaction.ex @@ -1,25 +1,21 @@ -defmodule Explorer.Chain.Zkevm.LifecycleTransaction do +defmodule Explorer.Chain.PolygonZkevm.LifecycleTransaction do @moduledoc "Models an L1 lifecycle transaction for zkEVM." use Explorer.Schema alias Explorer.Chain.Hash - alias Explorer.Chain.Zkevm.TransactionBatch + alias Explorer.Chain.PolygonZkevm.TransactionBatch @required_attrs ~w(id hash is_verify)a - @type t :: %__MODULE__{ - hash: Hash.t(), - is_verify: boolean() - } + @primary_key false + typed_schema "polygon_zkevm_lifecycle_l1_transactions" do + field(:id, :integer, primary_key: true, null: false) + field(:hash, Hash.Full, null: false) + field(:is_verify, :boolean, null: false) - @primary_key {:id, :integer, autogenerate: false} - schema "zkevm_lifecycle_l1_transactions" do - field(:hash, Hash.Full) - field(:is_verify, :boolean) - - has_many(:sequenced_batches, TransactionBatch, foreign_key: :sequence_id) - has_many(:verified_batches, TransactionBatch, foreign_key: :verify_id) + has_many(:sequenced_batches, TransactionBatch, foreign_key: :sequence_id, references: :id) + has_many(:verified_batches, TransactionBatch, foreign_key: :verify_id, references: :id) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex new file mode 100644 index 000000000000..60a8d2b8ed8f --- /dev/null +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex @@ -0,0 +1,321 @@ +defmodule Explorer.Chain.PolygonZkevm.Reader do + @moduledoc "Contains read functions for zkevm modules." + + import Ecto.Query, + only: [ + from: 2, + limit: 2, + order_by: 2, + where: 2, + where: 3 + ] + + import Explorer.Chain, only: [select_repo: 1] + + alias Explorer.Chain.PolygonZkevm.{BatchTransaction, Bridge, BridgeL1Token, LifecycleTransaction, TransactionBatch} + alias Explorer.{Chain, PagingOptions, Repo} + alias Indexer.Helper + + @doc """ + Reads a batch by its number from database. + If the number is :latest, gets the latest batch from `polygon_zkevm_transaction_batches` table. + Returns {:error, :not_found} in case the batch is not found. + """ + @spec batch(non_neg_integer() | :latest, list()) :: {:ok, map()} | {:error, :not_found} + def batch(number, options \\ []) + + def batch(:latest, options) when is_list(options) do + TransactionBatch + |> order_by(desc: :number) + |> limit(1) + |> select_repo(options).one() + |> case do + nil -> {:error, :not_found} + batch -> {:ok, batch} + end + end + + def batch(number, options) when is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + TransactionBatch + |> where(number: ^number) + |> Chain.join_associations(necessity_by_association) + |> select_repo(options).one() + |> case do + nil -> {:error, :not_found} + batch -> {:ok, batch} + end + end + + @doc """ + Reads a list of batches from `polygon_zkevm_transaction_batches` table. + """ + @spec batches(list()) :: list() + def batches(options \\ []) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + base_query = + from(tb in TransactionBatch, + order_by: [desc: tb.number] + ) + + query = + if Keyword.get(options, :confirmed?, false) do + base_query + |> Chain.join_associations(necessity_by_association) + |> where([tb], not is_nil(tb.sequence_id) and tb.sequence_id > 0) + |> limit(10) + else + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + base_query + |> Chain.join_associations(necessity_by_association) + |> page_batches(paging_options) + |> limit(^paging_options.page_size) + end + + select_repo(options).all(query) + end + + @doc """ + Reads a list of L2 transaction hashes from `polygon_zkevm_batch_l2_transactions` table. + """ + @spec batch_transactions(non_neg_integer(), list()) :: list() + def batch_transactions(batch_number, options \\ []) do + query = from(bts in BatchTransaction, where: bts.batch_number == ^batch_number) + + select_repo(options).all(query) + end + + @doc """ + Tries to read L1 token data (address, symbol, decimals) for the given addresses + from the database. If the data for an address is not found in Explorer.Chain.PolygonZkevm.BridgeL1Token, + the address is returned in the list inside the tuple (the second item of the tuple). + The first item of the returned tuple contains `L1 token address -> L1 token data` map. + """ + @spec get_token_data_from_db(list()) :: {map(), list()} + def get_token_data_from_db(token_addresses) do + # try to read token symbols and decimals from the database + query = + from( + t in BridgeL1Token, + where: t.address in ^token_addresses, + select: {t.address, t.decimals, t.symbol} + ) + + token_data = + query + |> Repo.all() + |> Enum.reduce(%{}, fn {address, decimals, symbol}, acc -> + token_address = Helper.address_hash_to_string(address, true) + Map.put(acc, token_address, %{symbol: symbol, decimals: decimals}) + end) + + token_addresses_for_rpc = + token_addresses + |> Enum.reject(fn address -> + Map.has_key?(token_data, Helper.address_hash_to_string(address, true)) + end) + + {token_data, token_addresses_for_rpc} + end + + @doc """ + Gets last known L1 item (deposit) from polygon_zkevm_bridge table. + Returns block number and L1 transaction hash bound to that deposit. + If not found, returns zero block number and nil as the transaction hash. + """ + @spec last_l1_item() :: {non_neg_integer(), binary() | nil} + def last_l1_item do + query = + from(b in Bridge, + select: {b.block_number, b.l1_transaction_hash}, + where: b.type == :deposit and not is_nil(b.block_number), + order_by: [desc: b.index], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end + + @doc """ + Gets last known L2 item (withdrawal) from polygon_zkevm_bridge table. + Returns block number and L2 transaction hash bound to that withdrawal. + If not found, returns zero block number and nil as the transaction hash. + """ + @spec last_l2_item() :: {non_neg_integer(), binary() | nil} + def last_l2_item do + query = + from(b in Bridge, + select: {b.block_number, b.l2_transaction_hash}, + where: b.type == :withdrawal and not is_nil(b.block_number), + order_by: [desc: b.index], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end + + @doc """ + Gets the number of the latest batch with defined verify_id from `polygon_zkevm_transaction_batches` table. + Returns 0 if not found. + """ + @spec last_verified_batch_number() :: non_neg_integer() + def last_verified_batch_number do + query = + from(tb in TransactionBatch, + select: tb.number, + where: not is_nil(tb.verify_id), + order_by: [desc: tb.number], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||(0) + end + + @doc """ + Reads a list of L1 transactions by their hashes from `polygon_zkevm_lifecycle_l1_transactions` table. + """ + @spec lifecycle_transactions(list()) :: list() + def lifecycle_transactions(l1_tx_hashes) do + query = + from( + lt in LifecycleTransaction, + select: {lt.hash, lt.id}, + where: lt.hash in ^l1_tx_hashes + ) + + Repo.all(query, timeout: :infinity) + end + + @doc """ + Determines ID of the future lifecycle transaction by reading `polygon_zkevm_lifecycle_l1_transactions` table. + """ + @spec next_id() :: non_neg_integer() + def next_id do + query = + from(lt in LifecycleTransaction, + select: lt.id, + order_by: [desc: lt.id], + limit: 1 + ) + + last_id = + query + |> Repo.one() + |> Kernel.||(0) + + last_id + 1 + end + + @doc """ + Builds `L1 token address -> L1 token id` map for the given token addresses. + The info is taken from Explorer.Chain.PolygonZkevm.BridgeL1Token. + If an address is not in the table, it won't be in the resulting map. + """ + @spec token_addresses_to_ids_from_db(list()) :: map() + def token_addresses_to_ids_from_db(addresses) do + query = from(t in BridgeL1Token, select: {t.address, t.id}, where: t.address in ^addresses) + + query + |> Repo.all(timeout: :infinity) + |> Enum.reduce(%{}, fn {address, id}, acc -> + Map.put(acc, Helper.address_hash_to_string(address), id) + end) + end + + @doc """ + Retrieves a list of Polygon zkEVM deposits (completed and unclaimed) + sorted in descending order of the index. + """ + @spec deposits(list()) :: list() + def deposits(options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + base_query = + from( + b in Bridge, + left_join: t1 in assoc(b, :l1_token), + left_join: t2 in assoc(b, :l2_token), + where: b.type == :deposit and not is_nil(b.l1_transaction_hash), + preload: [l1_token: t1, l2_token: t2], + order_by: [desc: b.index] + ) + + base_query + |> page_deposits_or_withdrawals(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + @doc """ + Returns a total number of Polygon zkEVM deposits (completed and unclaimed). + """ + @spec deposits_count(list()) :: term() | nil + def deposits_count(options \\ []) do + query = + from( + b in Bridge, + where: b.type == :deposit and not is_nil(b.l1_transaction_hash) + ) + + select_repo(options).aggregate(query, :count, timeout: :infinity) + end + + @doc """ + Retrieves a list of Polygon zkEVM withdrawals (completed and unclaimed) + sorted in descending order of the index. + """ + @spec withdrawals(list()) :: list() + def withdrawals(options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + base_query = + from( + b in Bridge, + left_join: t1 in assoc(b, :l1_token), + left_join: t2 in assoc(b, :l2_token), + where: b.type == :withdrawal and not is_nil(b.l2_transaction_hash), + preload: [l1_token: t1, l2_token: t2], + order_by: [desc: b.index] + ) + + base_query + |> page_deposits_or_withdrawals(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + @doc """ + Returns a total number of Polygon zkEVM withdrawals (completed and unclaimed). + """ + @spec withdrawals_count(list()) :: term() | nil + def withdrawals_count(options \\ []) do + query = + from( + b in Bridge, + where: b.type == :withdrawal and not is_nil(b.l2_transaction_hash) + ) + + select_repo(options).aggregate(query, :count, timeout: :infinity) + end + + defp page_batches(query, %PagingOptions{key: nil}), do: query + + defp page_batches(query, %PagingOptions{key: {number}}) do + from(tb in query, where: tb.number < ^number) + end + + defp page_deposits_or_withdrawals(query, %PagingOptions{key: nil}), do: query + + defp page_deposits_or_withdrawals(query, %PagingOptions{key: {index}}) do + from(b in query, where: b.index < ^index) + end +end diff --git a/apps/explorer/lib/explorer/chain/zkevm/transaction_batch.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/transaction_batch.ex similarity index 51% rename from apps/explorer/lib/explorer/chain/zkevm/transaction_batch.ex rename to apps/explorer/lib/explorer/chain/polygon_zkevm/transaction_batch.ex index eda97e1d403c..92ca1dd21527 100644 --- a/apps/explorer/lib/explorer/chain/zkevm/transaction_batch.ex +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/transaction_batch.ex @@ -1,31 +1,18 @@ -defmodule Explorer.Chain.Zkevm.TransactionBatch do +defmodule Explorer.Chain.PolygonZkevm.TransactionBatch do @moduledoc "Models a batch of transactions for zkEVM." use Explorer.Schema alias Explorer.Chain.Hash - alias Explorer.Chain.Zkevm.{BatchTransaction, LifecycleTransaction} - - @optional_attrs ~w(sequence_id verify_id)a - - @required_attrs ~w(number timestamp l2_transactions_count global_exit_root acc_input_hash state_root)a - - @type t :: %__MODULE__{ - number: non_neg_integer(), - timestamp: DateTime.t(), - l2_transactions_count: non_neg_integer(), - global_exit_root: Hash.t(), - acc_input_hash: Hash.t(), - state_root: Hash.t(), - sequence_id: non_neg_integer() | nil, - sequence_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil, - verify_id: non_neg_integer() | nil, - verify_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil, - l2_transactions: %Ecto.Association.NotLoaded{} | [BatchTransaction.t()] - } - - @primary_key {:number, :integer, autogenerate: false} - schema "zkevm_transaction_batches" do + alias Explorer.Chain.PolygonZkevm.{BatchTransaction, LifecycleTransaction} + + @optional_attrs ~w(timestamp sequence_id verify_id)a + + @required_attrs ~w(number l2_transactions_count global_exit_root acc_input_hash state_root)a + + @primary_key false + typed_schema "polygon_zkevm_transaction_batches" do + field(:number, :integer, primary_key: true, null: false) field(:timestamp, :utc_datetime_usec) field(:l2_transactions_count, :integer) field(:global_exit_root, Hash.Full) @@ -40,7 +27,7 @@ defmodule Explorer.Chain.Zkevm.TransactionBatch do belongs_to(:verify_transaction, LifecycleTransaction, foreign_key: :verify_id, references: :id, type: :integer) - has_many(:l2_transactions, BatchTransaction, foreign_key: :batch_number) + has_many(:l2_transactions, BatchTransaction, foreign_key: :batch_number, references: :number) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index b2439545a424..ce64b8d0d2fd 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -4,6 +4,7 @@ defmodule Explorer.Chain.Search do """ import Ecto.Query, only: [ + dynamic: 2, from: 2, limit: 2, order_by: 3, @@ -13,16 +14,19 @@ defmodule Explorer.Chain.Search do ] import Explorer.Chain, only: [select_repo: 1] - + import Explorer.MicroserviceInterfaces.BENS, only: [ens_domain_name_lookup: 1] alias Explorer.{Chain, PagingOptions} alias Explorer.Tags.{AddressTag, AddressToTag} alias Explorer.Chain.{ Address, + Beacon.Blob, Block, + DenormalizationHelper, SmartContract, Token, - Transaction + Transaction, + UserOperation } @doc """ @@ -32,74 +36,111 @@ defmodule Explorer.Chain.Search do def joint_search(paging_options, offset, raw_string, options \\ []) do string = String.trim(raw_string) - case prepare_search_term(string) do - {:some, term} -> - tokens_query = search_token_query(term) - contracts_query = search_contract_query(term) - labels_query = search_label_query(term) - tx_query = search_tx_query(string) - address_query = search_address_query(string) - block_query = search_block_query(string) - - basic_query = - from( - tokens in subquery(tokens_query), - union: ^contracts_query, - union: ^labels_query - ) + ens_task = maybe_run_ens_task(paging_options, raw_string, options) + + result = + case prepare_search_term(string) do + {:some, term} -> + query = base_joint_query(string, term) + + ordered_query = + from(items in subquery(query), + order_by: [ + desc: items.priority, + desc_nulls_last: items.circulating_market_cap, + desc_nulls_last: items.exchange_rate, + desc_nulls_last: items.is_verified_via_admin_panel, + desc_nulls_last: items.holder_count, + asc: items.name, + desc: items.inserted_at + ], + limit: ^paging_options.page_size, + offset: ^offset + ) - query = - cond do - address_query -> - basic_query - |> union(^address_query) + paginated_ordered_query = + ordered_query + |> page_search_results(paging_options) - tx_query -> - basic_query - |> union(^tx_query) - |> union(^block_query) + search_results = select_repo(options).all(paginated_ordered_query) - block_query -> - basic_query - |> union(^block_query) + search_results + |> Enum.map(fn result -> + result + |> compose_result_checksummed_address_hash() + |> format_timestamp() + end) - true -> - basic_query - end + _ -> + [] + end - ordered_query = - from(items in subquery(query), - order_by: [ - desc: items.priority, - desc_nulls_last: items.circulating_market_cap, - desc_nulls_last: items.exchange_rate, - desc_nulls_last: items.is_verified_via_admin_panel, - desc_nulls_last: items.holder_count, - asc: items.name, - desc: items.inserted_at - ], - limit: ^paging_options.page_size, - offset: ^offset - ) + ens_result = (ens_task && await_ens_task(ens_task)) || [] - paginated_ordered_query = - ordered_query - |> page_search_results(paging_options) + result ++ ens_result + end - search_results = select_repo(options).all(paginated_ordered_query) + def base_joint_query(string, term) do + tokens_query = search_token_query(string, term) + contracts_query = search_contract_query(term) + labels_query = search_label_query(term) + address_query = search_address_query(string) + block_query = search_block_query(string) + + basic_query = + from( + tokens in subquery(tokens_query), + union: ^contracts_query, + union: ^labels_query + ) - search_results - |> Enum.map(fn result -> - result - |> compose_result_checksummed_address_hash() - |> format_timestamp() - end) + cond do + address_query -> + basic_query + |> union(^address_query) - _ -> - [] + valid_full_hash?(string) -> + tx_query = search_tx_query(string) + + tx_block_query = + basic_query + |> union(^tx_query) + |> union(^block_query) + + tx_block_op_query = + if UserOperation.enabled?() do + user_operation_query = search_user_operation_query(string) + + tx_block_query + |> union(^user_operation_query) + else + tx_block_query + end + + if Application.get_env(:explorer, :chain_type) == "ethereum" do + blob_query = search_blob_query(string) + + tx_block_op_query + |> union(^blob_query) + else + tx_block_op_query + end + + block_query -> + basic_query + |> union(^block_query) + + true -> + basic_query end end + defp maybe_run_ens_task(%PagingOptions{key: nil}, query_string, options) do + Task.async(fn -> search_ens_name(query_string, options) end) + end + + defp maybe_run_ens_task(_, _query_string, _options), do: nil + @doc """ Search function. Differences from joint_search/4: 1. Returns all the found categories (amount of results up to `paging_options.page_size`). @@ -108,14 +149,16 @@ defmodule Explorer.Chain.Search do 2. Results couldn't be paginated """ @spec balanced_unpaginated_search(PagingOptions.t(), binary(), [Chain.api?()] | []) :: list + # credo:disable-for-next-line def balanced_unpaginated_search(paging_options, raw_search_query, options \\ []) do search_query = String.trim(raw_search_query) + ens_task = Task.async(fn -> search_ens_name(raw_search_query, options) end) case prepare_search_term(search_query) do {:some, term} -> tokens_result = - term - |> search_token_query() + search_query + |> search_token_query(term) |> order_by([token], desc_nulls_last: token.circulating_market_cap, desc_nulls_last: token.fiat_value, @@ -142,8 +185,27 @@ defmodule Explorer.Chain.Search do |> select_repo(options).all() tx_result = - if query = search_tx_query(search_query) do - query + if valid_full_hash?(search_query) do + search_query + |> search_tx_query() + |> select_repo(options).all() + else + [] + end + + op_result = + if valid_full_hash?(search_query) && UserOperation.enabled?() do + search_query + |> search_user_operation_query() + |> select_repo(options).all() + else + [] + end + + blob_result = + if valid_full_hash?(search_query) && Application.get_env(:explorer, :chain_type) == "ethereum" do + search_query + |> search_blob_query() |> select_repo(options).all() else [] @@ -166,8 +228,20 @@ defmodule Explorer.Chain.Search do [] end + ens_result = await_ens_task(ens_task) + non_empty_lists = - [tokens_result, contracts_result, labels_result, tx_result, address_result, blocks_result] + [ + tokens_result, + contracts_result, + labels_result, + tx_result, + op_result, + blob_result, + address_result, + blocks_result, + ens_result + ] |> Enum.filter(fn list -> Enum.count(list) > 0 end) |> Enum.sort_by(fn list -> Enum.count(list) end, :asc) @@ -189,6 +263,16 @@ defmodule Explorer.Chain.Search do end end + defp await_ens_task(ens_task) do + case Task.yield(ens_task, 5000) || Task.shutdown(ens_task) do + {:ok, result} -> + result + + _ -> + [] + end + end + def prepare_search_term(string) do case Regex.scan(~r/[a-zA-Z0-9]+/, string) do [_ | _] = words -> @@ -204,6 +288,15 @@ defmodule Explorer.Chain.Search do end defp search_label_query(term) do + label_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([att, _, _], att.address_hash)) + |> Map.put(:type, "label") + |> Map.put(:name, dynamic([_, at, _], at.display_name)) + |> Map.put(:inserted_at, dynamic([att, _, _], att.inserted_at)) + |> Map.put(:verified, dynamic([_, _, smart_contract], not is_nil(smart_contract))) + |> Map.put(:priority, 1) + inner_query = from(tag in AddressTag, where: fragment("to_tsvector('english', ?) @@ to_tsquery(?)", tag.display_name, ^term), @@ -215,88 +308,74 @@ defmodule Explorer.Chain.Search do on: att.tag_id == at.id, left_join: smart_contract in SmartContract, on: att.address_hash == smart_contract.address_hash, - select: %{ - address_hash: att.address_hash, - tx_hash: fragment("CAST(NULL AS bytea)"), - block_hash: fragment("CAST(NULL AS bytea)"), - type: "label", - name: at.display_name, - symbol: ^nil, - holder_count: ^nil, - inserted_at: att.inserted_at, - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: fragment("NULL::timestamp without time zone"), - verified: not is_nil(smart_contract), - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 1, - is_verified_via_admin_panel: nil - } + select: ^label_search_fields ) end - defp search_token_query(term) do - from(token in Token, - left_join: smart_contract in SmartContract, - on: token.contract_address_hash == smart_contract.address_hash, - where: fragment("to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", token.symbol, token.name, ^term), - select: %{ - address_hash: token.contract_address_hash, - tx_hash: fragment("CAST(NULL AS bytea)"), - block_hash: fragment("CAST(NULL AS bytea)"), - type: "token", - name: token.name, - symbol: token.symbol, - holder_count: token.holder_count, - inserted_at: token.inserted_at, - block_number: 0, - icon_url: token.icon_url, - token_type: token.type, - timestamp: fragment("NULL::timestamp without time zone"), - verified: not is_nil(smart_contract), - exchange_rate: token.fiat_value, - total_supply: token.total_supply, - circulating_market_cap: token.circulating_market_cap, - priority: 0, - is_verified_via_admin_panel: token.is_verified_via_admin_panel - } - ) + defp search_token_query(string, term) do + token_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([token, _], token.contract_address_hash)) + |> Map.put(:type, "token") + |> Map.put(:name, dynamic([token, _], token.name)) + |> Map.put(:symbol, dynamic([token, _], token.symbol)) + |> Map.put(:holder_count, dynamic([token, _], token.holder_count)) + |> Map.put(:inserted_at, dynamic([token, _], token.inserted_at)) + |> Map.put(:icon_url, dynamic([token, _], token.icon_url)) + |> Map.put(:token_type, dynamic([token, _], token.type)) + |> Map.put(:verified, dynamic([_, smart_contract], not is_nil(smart_contract))) + |> Map.put(:exchange_rate, dynamic([token, _], token.fiat_value)) + |> Map.put(:total_supply, dynamic([token, _], token.total_supply)) + |> Map.put(:circulating_market_cap, dynamic([token, _], token.circulating_market_cap)) + |> Map.put(:is_verified_via_admin_panel, dynamic([token, _], token.is_verified_via_admin_panel)) + + case Chain.string_to_address_hash(string) do + {:ok, address_hash} -> + from(token in Token, + left_join: smart_contract in SmartContract, + on: token.contract_address_hash == smart_contract.address_hash, + where: token.contract_address_hash == ^address_hash, + select: ^token_search_fields + ) + + _ -> + from(token in Token, + left_join: smart_contract in SmartContract, + on: token.contract_address_hash == smart_contract.address_hash, + where: fragment("to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", token.symbol, token.name, ^term), + select: ^token_search_fields + ) + end end defp search_contract_query(term) do + contract_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([smart_contract, _], smart_contract.address_hash)) + |> Map.put(:type, "contract") + |> Map.put(:name, dynamic([smart_contract, _], smart_contract.name)) + |> Map.put(:inserted_at, dynamic([_, address], address.inserted_at)) + |> Map.put(:verified, true) + from(smart_contract in SmartContract, left_join: address in Address, on: smart_contract.address_hash == address.hash, where: fragment("to_tsvector('english', ?) @@ to_tsquery(?)", smart_contract.name, ^term), - select: %{ - address_hash: smart_contract.address_hash, - tx_hash: fragment("CAST(NULL AS bytea)"), - block_hash: fragment("CAST(NULL AS bytea)"), - type: "contract", - name: smart_contract.name, - symbol: ^nil, - holder_count: ^nil, - inserted_at: address.inserted_at, - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: fragment("NULL::timestamp without time zone"), - verified: true, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + select: ^contract_search_fields ) end defp search_address_query(term) do case Chain.string_to_address_hash(term) do {:ok, address_hash} -> + address_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([address, _], address.hash)) + |> Map.put(:type, "address") + |> Map.put(:name, dynamic([_, address_name], address_name.name)) + |> Map.put(:inserted_at, dynamic([_, address_name], address_name.inserted_at)) + |> Map.put(:verified, dynamic([address, _], address.verified)) + from(address in Address, left_join: address_name in subquery( @@ -308,26 +387,7 @@ defmodule Explorer.Chain.Search do ), on: address.hash == address_name.address_hash, where: address.hash == ^address_hash, - select: %{ - address_hash: address.hash, - tx_hash: fragment("CAST(NULL AS bytea)"), - block_hash: fragment("CAST(NULL AS bytea)"), - type: "address", - name: address_name.name, - symbol: ^nil, - holder_count: ^nil, - inserted_at: address.inserted_at, - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: fragment("NULL::timestamp without time zone"), - verified: address.verified, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + select: ^address_search_fields ) _ -> @@ -335,65 +395,92 @@ defmodule Explorer.Chain.Search do end end - defp search_tx_query(term) do - case Chain.string_to_transaction_hash(term) do - {:ok, tx_hash} -> - from(transaction in Transaction, - left_join: block in Block, - on: transaction.block_hash == block.hash, - where: transaction.hash == ^tx_hash, - select: %{ - address_hash: fragment("CAST(NULL AS bytea)"), - tx_hash: transaction.hash, - block_hash: fragment("CAST(NULL AS bytea)"), - type: "transaction", - name: ^nil, - symbol: ^nil, - holder_count: ^nil, - inserted_at: transaction.inserted_at, - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: block.timestamp, - verified: nil, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } - ) + defp valid_full_hash?(string_input) do + case Chain.string_to_transaction_hash(string_input) do + {:ok, _tx_hash} -> true + _ -> false + end + end - _ -> - nil + defp search_tx_query(term) do + if DenormalizationHelper.transactions_denormalization_finished?() do + transaction_search_fields = + search_fields() + |> Map.put(:tx_hash, dynamic([transaction], transaction.hash)) + |> Map.put(:block_hash, dynamic([transaction], transaction.block_hash)) + |> Map.put(:type, "transaction") + |> Map.put(:block_number, dynamic([transaction], transaction.block_number)) + |> Map.put(:inserted_at, dynamic([transaction], transaction.inserted_at)) + |> Map.put(:timestamp, dynamic([transaction], transaction.block_timestamp)) + + from(transaction in Transaction, + where: transaction.hash == ^term, + select: ^transaction_search_fields + ) + else + transaction_search_fields = + search_fields() + |> Map.put(:tx_hash, dynamic([transaction, _], transaction.hash)) + |> Map.put(:block_hash, dynamic([transaction, _], transaction.block_hash)) + |> Map.put(:type, "transaction") + |> Map.put(:block_number, dynamic([transaction, _], transaction.block_number)) + |> Map.put(:inserted_at, dynamic([transaction, _], transaction.inserted_at)) + |> Map.put(:timestamp, dynamic([_, block], block.timestamp)) + + from(transaction in Transaction, + left_join: block in Block, + on: transaction.block_hash == block.hash, + where: transaction.hash == ^term, + select: ^transaction_search_fields + ) end end + defp search_user_operation_query(term) do + user_operation_search_fields = + search_fields() + |> Map.put(:user_operation_hash, dynamic([user_operation, _], user_operation.hash)) + |> Map.put(:block_hash, dynamic([user_operation, _], user_operation.block_hash)) + |> Map.put(:type, "user_operation") + |> Map.put(:inserted_at, dynamic([user_operation, _], user_operation.inserted_at)) + |> Map.put(:block_number, dynamic([user_operation, _], user_operation.block_number)) + |> Map.put(:timestamp, dynamic([_, block], block.timestamp)) + + from(user_operation in UserOperation, + left_join: block in Block, + on: user_operation.block_hash == block.hash, + where: user_operation.hash == ^term, + select: ^user_operation_search_fields + ) + end + + defp search_blob_query(term) do + blob_search_fields = + search_fields() + |> Map.put(:blob_hash, dynamic([blob, _], blob.hash)) + |> Map.put(:type, "blob") + |> Map.put(:inserted_at, dynamic([blob, _], blob.inserted_at)) + + from(blob in Blob, + where: blob.hash == ^term, + select: ^blob_search_fields + ) + end + defp search_block_query(term) do + block_search_fields = + search_fields() + |> Map.put(:block_hash, dynamic([block], block.hash)) + |> Map.put(:type, "block") + |> Map.put(:block_number, dynamic([block], block.number)) + |> Map.put(:inserted_at, dynamic([block], block.inserted_at)) + |> Map.put(:timestamp, dynamic([block], block.timestamp)) + case Chain.string_to_block_hash(term) do {:ok, block_hash} -> from(block in Block, where: block.hash == ^block_hash, - select: %{ - address_hash: fragment("CAST(NULL AS bytea)"), - tx_hash: fragment("CAST(NULL AS bytea)"), - block_hash: block.hash, - type: "block", - name: ^nil, - symbol: ^nil, - holder_count: ^nil, - inserted_at: block.inserted_at, - block_number: block.number, - icon_url: nil, - token_type: nil, - timestamp: block.timestamp, - verified: nil, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + select: ^block_search_fields ) _ -> @@ -401,26 +488,7 @@ defmodule Explorer.Chain.Search do {block_number, ""} -> from(block in Block, where: block.number == ^block_number, - select: %{ - address_hash: fragment("CAST(NULL AS bytea)"), - tx_hash: fragment("CAST(NULL AS bytea)"), - block_hash: block.hash, - type: "block", - name: ^nil, - symbol: ^nil, - holder_count: ^nil, - inserted_at: block.inserted_at, - block_number: block.number, - icon_url: nil, - token_type: nil, - timestamp: block.timestamp, - verified: nil, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + select: ^block_search_fields ) _ -> @@ -520,4 +588,58 @@ defmodule Explorer.Chain.Search do result end end + + defp search_ens_name(search_query, options) do + trimmed_query = String.trim(search_query) + + with true <- Regex.match?(~r/\w+\.\w+/, trimmed_query), + result when is_map(result) <- ens_domain_name_lookup(search_query) do + [ + result[:address_hash] + |> search_address_query() + |> select_repo(options).all() + |> merge_address_search_result_with_ens_info(result) + ] + else + _ -> + [] + end + end + + defp merge_address_search_result_with_ens_info([], ens_info) do + search_fields() + |> Map.put(:address_hash, ens_info[:address_hash]) + |> Map.put(:type, "address") + |> Map.put(:ens_info, ens_info) + |> Map.put(:timestamp, nil) + end + + defp merge_address_search_result_with_ens_info([address], ens_info) do + Map.put(address |> compose_result_checksummed_address_hash(), :ens_info, ens_info) + end + + defp search_fields do + %{ + address_hash: dynamic([_], type(^nil, :binary)), + tx_hash: dynamic([_], type(^nil, :binary)), + user_operation_hash: dynamic([_], type(^nil, :binary)), + blob_hash: dynamic([_], type(^nil, :binary)), + block_hash: dynamic([_], type(^nil, :binary)), + type: nil, + name: nil, + symbol: nil, + holder_count: nil, + inserted_at: nil, + block_number: 0, + icon_url: nil, + token_type: nil, + timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)), + verified: nil, + exchange_rate: nil, + total_supply: nil, + circulating_market_cap: nil, + priority: 0, + is_verified_via_admin_panel: nil + } + end end diff --git a/apps/explorer/lib/explorer/chain/shibarium/bridge.ex b/apps/explorer/lib/explorer/chain/shibarium/bridge.ex new file mode 100644 index 000000000000..374bf2ae8d37 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/shibarium/bridge.ex @@ -0,0 +1,67 @@ +defmodule Explorer.Chain.Shibarium.Bridge do + @moduledoc "Models Shibarium Bridge operation." + + use Explorer.Schema + + alias Explorer.Chain.{ + Address, + Block, + Hash, + Transaction + } + + @optional_attrs ~w(amount_or_id erc1155_ids erc1155_amounts l1_transaction_hash l1_block_number l2_transaction_hash l2_block_number timestamp)a + + @required_attrs ~w(user operation_hash operation_type token_type)a + + @allowed_attrs @optional_attrs ++ @required_attrs + + @typedoc """ + * `user_address` - address of the user that initiated operation + * `user` - foreign key of `user_address` + * `amount_or_id` - amount of the operation or NFT id (in case of ERC-721 token) + * `erc1155_ids` - an array of ERC-1155 token ids (when batch ERC-1155 token transfer) + * `erc1155_amounts` - an array of corresponding ERC-1155 token amounts (when batch ERC-1155 token transfer) + * `l1_transaction_hash` - transaction hash for L1 side + * `l1_block_number` - block number of `l1_transaction` + * `l2_transaction` - transaction hash for L2 side + * `l2_transaction_hash` - foreign key of `l2_transaction` + * `l2_block_number` - block number of `l2_transaction` + * `operation_hash` - keccak256 hash of the operation calculated as follows: ExKeccak.hash_256(user, amount_or_id, erc1155_ids, erc1155_amounts, operation_id) + * `operation_type` - `deposit` or `withdrawal` + * `token_type` - `bone` or `eth` or `other` + * `timestamp` - timestamp of the operation block (L1 block for deposit, L2 block - for withdrawal) + """ + @primary_key false + typed_schema "shibarium_bridge" do + belongs_to(:user_address, Address, foreign_key: :user, references: :hash, type: Hash.Address, null: false) + field(:amount_or_id, :decimal) + field(:erc1155_ids, {:array, :decimal}) + field(:erc1155_amounts, {:array, :decimal}) + field(:operation_hash, Hash.Full, primary_key: true, null: false) + field(:operation_type, Ecto.Enum, values: [:deposit, :withdrawal], null: false) + field(:l1_transaction_hash, Hash.Full, primary_key: true) + field(:l1_block_number, :integer) :: Block.block_number() | nil + + belongs_to(:l2_transaction, Transaction, + foreign_key: :l2_transaction_hash, + references: :hash, + type: Hash.Full, + primary_key: true + ) + + field(:l2_block_number, :integer) :: Block.block_number() | nil + field(:token_type, Ecto.Enum, values: [:bone, :eth, :other], null: false) + field(:timestamp, :utc_datetime_usec) + + timestamps() + end + + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = module, attrs \\ %{}) do + module + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + |> unique_constraint([:operation_hash, :l1_transaction_hash, :l2_transaction_hash]) + end +end diff --git a/apps/explorer/lib/explorer/chain/shibarium/reader.ex b/apps/explorer/lib/explorer/chain/shibarium/reader.ex new file mode 100644 index 000000000000..f452c5dced7f --- /dev/null +++ b/apps/explorer/lib/explorer/chain/shibarium/reader.ex @@ -0,0 +1,108 @@ +defmodule Explorer.Chain.Shibarium.Reader do + @moduledoc "Contains read functions for Shibarium modules." + + import Ecto.Query, + only: [ + from: 2, + limit: 2 + ] + + import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] + + alias Explorer.Chain.Shibarium.Bridge + alias Explorer.PagingOptions + + @doc """ + Returns a list of completed Shibarium deposits to display them in UI. + """ + @spec deposits(list()) :: list() + def deposits(options \\ []) do + paging_options = Keyword.get(options, :paging_options, default_paging_options()) + + base_query = + from( + sb in Bridge, + where: sb.operation_type == :deposit and not is_nil(sb.l1_block_number) and not is_nil(sb.l2_block_number), + select: %{ + l1_block_number: sb.l1_block_number, + l1_transaction_hash: sb.l1_transaction_hash, + l2_transaction_hash: sb.l2_transaction_hash, + user: sb.user, + timestamp: sb.timestamp + }, + order_by: [desc: sb.l1_block_number] + ) + + base_query + |> page_deposits(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + @doc """ + Returns a total number of completed Shibarium deposits. + """ + @spec deposits_count(list()) :: term() | nil + def deposits_count(options \\ []) do + query = + from( + sb in Bridge, + where: sb.operation_type == :deposit and not is_nil(sb.l1_block_number) and not is_nil(sb.l2_block_number) + ) + + select_repo(options).aggregate(query, :count, timeout: :infinity) + end + + @doc """ + Returns a list of completed Shibarium withdrawals to display them in UI. + """ + @spec withdrawals(list()) :: list() + def withdrawals(options \\ []) do + paging_options = Keyword.get(options, :paging_options, default_paging_options()) + + base_query = + from( + sb in Bridge, + where: sb.operation_type == :withdrawal and not is_nil(sb.l1_block_number) and not is_nil(sb.l2_block_number), + select: %{ + l2_block_number: sb.l2_block_number, + l2_transaction_hash: sb.l2_transaction_hash, + l1_transaction_hash: sb.l1_transaction_hash, + user: sb.user, + timestamp: sb.timestamp + }, + order_by: [desc: sb.l2_block_number] + ) + + base_query + |> page_withdrawals(paging_options) + |> limit(^paging_options.page_size) + |> select_repo(options).all() + end + + @doc """ + Returns a total number of completed Shibarium withdrawals. + """ + @spec withdrawals_count(list()) :: term() | nil + def withdrawals_count(options \\ []) do + query = + from( + sb in Bridge, + where: sb.operation_type == :withdrawal and not is_nil(sb.l1_block_number) and not is_nil(sb.l2_block_number) + ) + + select_repo(options).aggregate(query, :count, timeout: :infinity) + end + + defp page_deposits(query, %PagingOptions{key: nil}), do: query + + defp page_deposits(query, %PagingOptions{key: {block_number}}) do + from(item in query, where: item.l1_block_number < ^block_number) + end + + defp page_withdrawals(query, %PagingOptions{key: nil}), do: query + + defp page_withdrawals(query, %PagingOptions{key: {block_number}}) do + from(item in query, where: item.l2_block_number < ^block_number) + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 18590460e672..2c97ae84cb52 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -12,27 +12,36 @@ defmodule Explorer.Chain.SmartContract do use Explorer.Schema - alias Ecto.Changeset - alias EthereumJSONRPC.Contract + alias Ecto.{Changeset, Multi} alias Explorer.Counters.AverageBlockTime - alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Address, ContractMethod, DecompiledSmartContract, Hash} - alias Explorer.Chain.SmartContract.ExternalLibrary - alias Explorer.SmartContract.Reader + alias Explorer.{Chain, Repo, SortingHelper} + + alias Explorer.Chain.{ + Address, + ContractMethod, + Data, + DecompiledSmartContract, + Hash, + InternalTransaction, + SmartContract, + SmartContractAdditionalSource, + Transaction + } + + alias Explorer.Chain.Address.Name, as: AddressName + + alias Explorer.Chain.SmartContract.{ExternalLibrary, Proxy} + alias Explorer.Chain.SmartContract.Proxy.EIP1167 + alias Explorer.SmartContract.Helper + alias Explorer.SmartContract.Solidity.Verifier alias Timex.Duration - # supported signatures: - # 5c60da1b = keccak256(implementation()) - @implementation_signature "5c60da1b" - # aaf10f42 = keccak256(getImplementation()) - @get_implementation_signature "aaf10f42" + @typep api? :: {:api?, true | false} @burn_address_hash_string "0x0000000000000000000000000000000000000000" @burn_address_hash_string_32 "0x0000000000000000000000000000000000000000000000000000000000000000" - defguard is_burn_signature(term) when term in ["0x", "0x0", @burn_address_hash_string_32] - defguard is_burn_signature_or_nil(term) when is_burn_signature(term) or term == nil - defguard is_burn_signature_extended(term) when is_burn_signature(term) or term == @burn_address_hash_string + defguard is_burn_signature(term) when term in ["0x", "0x0", @burn_address_hash_string, @burn_address_hash_string_32] @doc """ Returns burn address hash @@ -42,7 +51,7 @@ defmodule Explorer.Chain.SmartContract do @burn_address_hash_string end - @typep api? :: {:api?, true | false} + @default_sorting [desc: :id] @typedoc """ The name of a parameter to a function or event. @@ -210,6 +219,39 @@ defmodule Explorer.Chain.SmartContract do """ @type abi :: [event_description | function_description] + @doc """ + 1. No License (None) + 2. The Unlicense (Unlicense) + 3. MIT License (MIT) + 4. GNU General Public License v2.0 (GNU GPLv2) + 5. GNU General Public License v3.0 (GNU GPLv3) + 6. GNU Lesser General Public License v2.1 (GNU LGPLv2.1) + 7. GNU Lesser General Public License v3.0 (GNU LGPLv3) + 8. BSD 2-clause "Simplified" license (BSD-2-Clause) + 9. BSD 3-clause "New" Or "Revised" license* (BSD-3-Clause) + 10. Mozilla Public License 2.0 (MPL-2.0) + 11. Open Software License 3.0 (OSL-3.0) + 12. Apache 2.0 (Apache-2.0) + 13. GNU Affero General Public License (GNU AGPLv3) + 14. Business Source License (BSL 1.1) + """ + @license_enum [ + none: 1, + unlicense: 2, + mit: 3, + gnu_gpl_v2: 4, + gnu_gpl_v3: 5, + gnu_lgpl_v2_1: 6, + gnu_lgpl_v3: 7, + bsd_2_clause: 8, + bsd_3_clause: 9, + mpl_2_0: 10, + osl_3_0: 11, + apache_2_0: 12, + gnu_agpl_v3: 13, + bsl_1_1: 14 + ] + @typedoc """ * `name` - the human-readable name of the smart contract. * `compiler_version` - the version of the Solidity compiler used to compile `contract_source_code` with `optimization` @@ -235,41 +277,15 @@ defmodule Explorer.Chain.SmartContract do * `is_yul` - field was added for storing user's choice * `verified_via_eth_bytecode_db` - whether contract automatically verified via eth-bytecode-db or not. """ - - @type t :: %Explorer.Chain.SmartContract{ - name: String.t(), - compiler_version: String.t(), - optimization: boolean, - contract_source_code: String.t(), - constructor_arguments: String.t() | nil, - evm_version: String.t() | nil, - optimization_runs: non_neg_integer() | nil, - abi: [function_description], - verified_via_sourcify: boolean | nil, - partially_verified: boolean | nil, - file_path: String.t(), - is_vyper_contract: boolean | nil, - is_changed_bytecode: boolean, - bytecode_checked_at: DateTime.t(), - contract_code_md5: String.t(), - implementation_name: String.t() | nil, - compiler_settings: map() | nil, - implementation_fetched_at: DateTime.t(), - implementation_address_hash: Hash.Address.t(), - autodetect_constructor_args: boolean | nil, - is_yul: boolean | nil, - verified_via_eth_bytecode_db: boolean | nil - } - - schema "smart_contracts" do - field(:name, :string) - field(:compiler_version, :string) - field(:optimization, :boolean) - field(:contract_source_code, :string) + typed_schema "smart_contracts" do + field(:name, :string, null: false) + field(:compiler_version, :string, null: false) + field(:optimization, :boolean, null: false) + field(:contract_source_code, :string, null: false) field(:constructor_arguments, :string) field(:evm_version, :string) field(:optimization_runs, :integer) - embeds_many(:external_libraries, ExternalLibrary) + embeds_many(:external_libraries, ExternalLibrary, on_replace: :delete) field(:abi, {:array, :map}) field(:verified_via_sourcify, :boolean) field(:partially_verified, :boolean) @@ -277,7 +293,7 @@ defmodule Explorer.Chain.SmartContract do field(:is_vyper_contract, :boolean) field(:is_changed_bytecode, :boolean, default: false) field(:bytecode_checked_at, :utc_datetime_usec, default: DateTime.add(DateTime.utc_now(), -86400, :second)) - field(:contract_code_md5, :string) + field(:contract_code_md5, :string, null: false) field(:implementation_name, :string) field(:compiler_settings, :map) field(:implementation_fetched_at, :utc_datetime_usec, default: nil) @@ -286,6 +302,7 @@ defmodule Explorer.Chain.SmartContract do field(:is_yul, :boolean, virtual: true) field(:metadata_from_verified_twin, :boolean, virtual: true) field(:verified_via_eth_bytecode_db, :boolean) + field(:license_type, Ecto.Enum, values: @license_enum, default: :none) has_many( :decompiled_smart_contracts, @@ -298,7 +315,13 @@ defmodule Explorer.Chain.SmartContract do Address, foreign_key: :address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false + ) + + has_many(:smart_contract_additional_sources, SmartContractAdditionalSource, + references: :address_hash, + foreign_key: :address_hash ) timestamps() @@ -331,7 +354,8 @@ defmodule Explorer.Chain.SmartContract do :compiler_settings, :implementation_address_hash, :implementation_fetched_at, - :verified_via_eth_bytecode_db + :verified_via_eth_bytecode_db, + :license_type ]) |> validate_required([ :name, @@ -372,7 +396,8 @@ defmodule Explorer.Chain.SmartContract do :contract_code_md5, :implementation_name, :autodetect_constructor_args, - :verified_via_eth_bytecode_db + :verified_via_eth_bytecode_db, + :license_type ]) |> (&if(verification_with_files?, do: &1, @@ -427,79 +452,6 @@ defmodule Explorer.Chain.SmartContract do end end - defp upsert_contract_methods(%Changeset{changes: %{abi: abi}} = changeset) do - ContractMethod.upsert_from_abi(abi, get_field(changeset, :address_hash)) - - changeset - rescue - exception -> - message = Exception.format(:error, exception, __STACKTRACE__) - - Logger.error(fn -> ["Error while upserting contract methods: ", message] end) - - changeset - end - - defp upsert_contract_methods(changeset), do: changeset - - defp error_message(:compilation), do: error_message_with_log("There was an error compiling your contract.") - - defp error_message(:compiler_version), - do: error_message_with_log("Compiler version does not match, please try again.") - - defp error_message(:generated_bytecode), do: error_message_with_log("Bytecode does not match, please try again.") - - defp error_message(:constructor_arguments), - do: error_message_with_log("Constructor arguments do not match, please try again.") - - defp error_message(:name), do: error_message_with_log("Wrong contract name, please try again.") - defp error_message(:json), do: error_message_with_log("Invalid JSON file.") - - defp error_message(:autodetect_constructor_arguments_failed), - do: - error_message_with_log( - "Autodetection of constructor arguments failed. Please try to input constructor arguments manually." - ) - - defp error_message(:no_creation_data), - do: - error_message_with_log( - "The contract creation transaction has not been indexed yet. Please wait a few minutes and try again." - ) - - defp error_message(:unknown_error), do: error_message_with_log("Unable to verify: unknown error.") - - defp error_message(:deployed_bytecode), - do: error_message_with_log("Deployed bytecode does not correspond to contract creation code.") - - defp error_message(:contract_source_code), do: error_message_with_log("Empty contract source code.") - - defp error_message(string) when is_binary(string), do: error_message_with_log(string) - defp error_message(%{"message" => string} = error) when is_map(error), do: error_message_with_log(string) - - defp error_message(error) do - Logger.warn(fn -> ["Unknown verifier error: ", inspect(error)] end) - "There was an error validating your contract, please try again." - end - - defp error_message(:compilation, error_message), - do: error_message_with_log("There was an error compiling your contract: #{error_message}") - - defp error_message_with_log(error_string) do - Logger.error("Smart-contract verification error: #{error_string}") - error_string - end - - defp select_error_field(:no_creation_data), do: :address_hash - defp select_error_field(:compiler_version), do: :compiler_version - - defp select_error_field(constructor_arguments) - when constructor_arguments in [:constructor_arguments, :autodetect_constructor_arguments_failed], - do: :constructor_arguments - - defp select_error_field(:name), do: :name - defp select_error_field(_), do: :contract_source_code - def merge_twin_contract_with_changeset(%__MODULE__{} = twin_contract, %Changeset{} = changeset) do %__MODULE__{} |> changeset(Map.from_struct(twin_contract)) @@ -544,6 +496,12 @@ defmodule Explorer.Chain.SmartContract do |> Changeset.put_change(:contract_source_code, "") end + def license_types_enum, do: @license_enum + + @doc """ + Returns smart-contract changeset with checksummed address hash + """ + @spec address_to_checksum_address(Changeset.t()) :: Changeset.t() def address_to_checksum_address(changeset) do checksum_address = changeset @@ -554,36 +512,10 @@ defmodule Explorer.Chain.SmartContract do Changeset.force_change(changeset, :address_hash, checksum_address) end - defp to_address_hash(string) when is_binary(string) do - {:ok, address_hash} = Chain.string_to_address_hash(string) - address_hash - end - - defp to_address_hash(address_hash), do: address_hash - - def proxy_contract?(smart_contract, options \\ []) - - def proxy_contract?(%__MODULE__{abi: abi} = smart_contract, options) when not is_nil(abi) do - implementation_method_abi = - abi - |> Enum.find(fn method -> - Map.get(method, "name") == "implementation" || - Chain.master_copy_pattern?(method) - end) - - if implementation_method_abi || - not is_nil( - smart_contract - |> get_implementation_address_hash(options) - |> Tuple.to_list() - |> List.first() - ), - do: true, - else: false - end - - def proxy_contract?(_, _), do: false - + @doc """ + Returns implementation address and name of the given SmartContract by hash address + """ + @spec get_implementation_address_hash(any(), any()) :: {any(), any()} def get_implementation_address_hash(smart_contract, options \\ []) def get_implementation_address_hash(%__MODULE__{abi: nil}, _), do: {nil, nil} @@ -600,14 +532,14 @@ defmodule Explorer.Chain.SmartContract do options ) do updated_smart_contract = - if Application.get_env(:explorer, :enable_caching_implementation_data_of_proxy) && + if Application.get_env(:explorer, :proxy)[:caching_implementation_data_enabled] && check_implementation_refetch_necessity(implementation_fetched_at) do - Chain.address_hash_to_smart_contract_without_twin(address_hash, options) + address_hash_to_smart_contract_without_twin(address_hash, options) else smart_contract end - get_implementation_address_hash({:updated, updated_smart_contract}) + get_implementation_address_hash({:updated, updated_smart_contract}, options) end def get_implementation_address_hash( @@ -624,9 +556,18 @@ defmodule Explorer.Chain.SmartContract do ) do if check_implementation_refetch_necessity(implementation_fetched_at) do get_implementation_address_hash_task = - Task.async(fn -> get_implementation_address_hash(address_hash, abi, metadata_from_verified_twin, options) end) + Task.async(fn -> + result = Proxy.fetch_implementation_address_hash(address_hash, abi, metadata_from_verified_twin, options) + callback = Keyword.get(options, :callback, nil) + uid = Keyword.get(options, :uid) + + callback && callback.(result, uid) + + result + end) - timeout = Application.get_env(:explorer, :implementation_data_fetching_timeout) + timeout = + Keyword.get(options, :timeout, Application.get_env(:explorer, :proxy)[:implementation_data_fetching_timeout]) case Task.yield(get_implementation_address_hash_task, timeout) || Task.ignore(get_implementation_address_hash_task) do @@ -648,321 +589,747 @@ defmodule Explorer.Chain.SmartContract do def get_implementation_address_hash(_, _), do: {nil, nil} - defp db_implementation_data_converter(nil), do: nil - defp db_implementation_data_converter(string) when is_binary(string), do: string - defp db_implementation_data_converter(other), do: to_string(other) + def save_implementation_data(nil, _, _, _), do: {nil, nil} + + def save_implementation_data(empty_address_hash_string, proxy_address_hash, metadata_from_verified_twin, options) + when is_burn_signature(empty_address_hash_string) do + if is_nil(metadata_from_verified_twin) or !metadata_from_verified_twin do + proxy_address_hash + |> address_hash_to_smart_contract_without_twin(options) + |> changeset(%{ + implementation_name: nil, + implementation_address_hash: nil, + implementation_fetched_at: DateTime.utc_now() + }) + |> Repo.update() + end - defp check_implementation_refetch_necessity(nil), do: true + {:empty, :empty} + end - defp check_implementation_refetch_necessity(timestamp) do - if Application.get_env(:explorer, :enable_caching_implementation_data_of_proxy) do - now = DateTime.utc_now() + def save_implementation_data(implementation_address_hash_string, proxy_address_hash, _, options) + when is_binary(implementation_address_hash_string) do + with {:ok, address_hash} <- Chain.string_to_address_hash(implementation_address_hash_string), + proxy_contract <- address_hash_to_smart_contract_without_twin(proxy_address_hash, options), + false <- is_nil(proxy_contract), + %{implementation: %__MODULE__{name: name}, proxy: proxy_contract} <- %{ + implementation: address_hash_to_smart_contract(address_hash, options), + proxy: proxy_contract + } do + proxy_contract + |> changeset(%{ + implementation_name: name, + implementation_address_hash: implementation_address_hash_string, + implementation_fetched_at: DateTime.utc_now() + }) + |> Repo.update() + + {implementation_address_hash_string, name} + else + %{implementation: _, proxy: proxy_contract} -> + proxy_contract + |> changeset(%{ + implementation_name: nil, + implementation_address_hash: implementation_address_hash_string, + implementation_fetched_at: DateTime.utc_now() + }) + |> Repo.update() - average_block_time = get_average_block_time() + {implementation_address_hash_string, nil} - fresh_time_distance = - case average_block_time do - 0 -> - Application.get_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy) + true -> + {:ok, address_hash} = Chain.string_to_address_hash(implementation_address_hash_string) + smart_contract = address_hash_to_smart_contract(address_hash, options) - time -> - round(time) - end + {implementation_address_hash_string, smart_contract && smart_contract.name} - timestamp - |> DateTime.add(fresh_time_distance, :millisecond) - |> DateTime.compare(now) != :gt - else - true + _ -> + {implementation_address_hash_string, nil} end end - defp get_average_block_time do - if Application.get_env(:explorer, :avg_block_time_as_ttl_cached_implementation_data_of_proxy) do - case AverageBlockTime.average_block_time() do - {:error, :disabled} -> - 0 + @doc """ + Returns SmartContract by the given smart-contract address hash, if it is partially verified + """ + @spec select_partially_verified_by_address_hash(binary() | Hash.t(), keyword) :: boolean() | nil + def select_partially_verified_by_address_hash(address_hash, options \\ []) do + query = + from( + smart_contract in __MODULE__, + where: smart_contract.address_hash == ^address_hash, + select: smart_contract.partially_verified + ) - duration -> - duration - |> Duration.to_milliseconds() + Chain.select_repo(options).one(query) + end + + @doc """ + Extracts creation bytecode (`init`) and transaction (`tx`) or + internal transaction (`internal_tx`) where the contract was created. + """ + @spec creation_tx_with_bytecode(binary() | Hash.t()) :: + %{init: binary(), tx: Transaction.t()} | %{init: binary(), internal_tx: InternalTransaction.t()} | nil + def creation_tx_with_bytecode(address_hash) do + creation_tx_query = + from( + tx in Transaction, + where: tx.created_contract_address_hash == ^address_hash, + where: tx.status == ^1, + order_by: [desc: tx.block_number], + limit: ^1 + ) + + tx = + creation_tx_query + |> Repo.one() + + if tx do + with %{input: input} <- tx do + %{init: Data.to_string(input), tx: tx} end else - 0 + creation_int_tx_query = + from( + itx in InternalTransaction, + join: t in assoc(itx, :transaction), + where: itx.created_contract_address_hash == ^address_hash, + where: t.status == ^1 + ) + + internal_tx = creation_int_tx_query |> Repo.one() + + case internal_tx do + %{init: init} -> + init_str = Data.to_string(init) + %{init: init_str, internal_tx: internal_tx} + + _ -> + nil + end end end - @spec get_implementation_address_hash(Hash.Address.t(), list(), boolean() | nil, [api?]) :: - {String.t() | nil, String.t() | nil} - defp get_implementation_address_hash(proxy_address_hash, abi, metadata_from_verified_twin, options) - when not is_nil(proxy_address_hash) and not is_nil(abi) do - implementation_method_abi = - abi - |> Enum.find(fn method -> - Map.get(method, "name") == "implementation" && Map.get(method, "stateMutability") == "view" - end) + @doc """ + Composes address object for smart-contract + """ + @spec compose_smart_contract(map(), Hash.t(), any()) :: map() + def compose_smart_contract(address_result, hash, options) do + address_verified_twin_contract = + EIP1167.get_implementation_address(hash, options) || + get_address_verified_twin_contract(hash, options).verified_contract + + if address_verified_twin_contract do + address_verified_twin_contract_updated = + address_verified_twin_contract + |> Map.put(:address_hash, hash) + |> Map.put(:metadata_from_verified_twin, true) + |> Map.put(:implementation_address_hash, nil) + |> Map.put(:implementation_name, nil) + |> Map.put(:implementation_fetched_at, nil) + + address_result + |> Map.put(:smart_contract, address_verified_twin_contract_updated) + else + address_result + end + end + + @doc """ + Finds metadata for verification of a contract from verified twins: contracts with the same bytecode + which were verified previously, returns a single t:SmartContract.t/0 + """ + @spec get_address_verified_twin_contract(Hash.t() | String.t(), any()) :: %{ + :verified_contract => any(), + :additional_sources => SmartContractAdditionalSource.t() | nil + } + def get_address_verified_twin_contract(hash, options \\ []) + + def get_address_verified_twin_contract(hash, options) when is_binary(hash) do + case Chain.string_to_address_hash(hash) do + {:ok, address_hash} -> get_address_verified_twin_contract(address_hash, options) + _ -> %{:verified_contract => nil, :additional_sources => nil} + end + end + + def get_address_verified_twin_contract(%Hash{} = address_hash, options) do + with target_address <- Chain.select_repo(options).get(Address, address_hash), + false <- is_nil(target_address) do + verified_contract_twin = get_verified_twin_contract(target_address, options) + + verified_contract_twin_additional_sources = + SmartContractAdditionalSource.get_contract_additional_sources(verified_contract_twin, options) + + %{ + :verified_contract => check_and_update_constructor_args(verified_contract_twin), + :additional_sources => verified_contract_twin_additional_sources + } + else + _ -> + %{:verified_contract => nil, :additional_sources => nil} + end + end + + @doc """ + Returns verified smart-contract with the same bytecode of the given smart-contract + """ + @spec get_verified_twin_contract(Address.t(), any()) :: SmartContract.t() | nil + def get_verified_twin_contract(%Address{} = target_address, options \\ []) do + case target_address do + %{contract_code: %Chain.Data{bytes: contract_code_bytes}} -> + target_address_hash = target_address.hash + + contract_code_md5 = Helper.contract_code_md5(contract_code_bytes) + + verified_contract_twin_query = + from( + smart_contract in __MODULE__, + where: smart_contract.contract_code_md5 == ^contract_code_md5, + where: smart_contract.address_hash != ^target_address_hash, + select: smart_contract, + limit: 1 + ) + + verified_contract_twin_query + |> Chain.select_repo(options).one(timeout: 10_000) + + _ -> + nil + end + end + + @doc """ + Returns address or smart_contract object with parsed constructor_arguments + """ + @spec check_and_update_constructor_args(any()) :: any() + def check_and_update_constructor_args( + %__MODULE__{address_hash: address_hash, constructor_arguments: nil, verified_via_sourcify: true} = + smart_contract + ) do + if args = Verifier.parse_constructor_arguments_for_sourcify_contract(address_hash, smart_contract.abi) do + smart_contract |> __MODULE__.changeset(%{constructor_arguments: args}) |> Repo.update() + %__MODULE__{smart_contract | constructor_arguments: args} + else + smart_contract + end + end + + def check_and_update_constructor_args( + %Address{ + hash: address_hash, + contract_code: deployed_bytecode, + smart_contract: %__MODULE__{constructor_arguments: nil, verified_via_sourcify: true} = smart_contract + } = address + ) do + if args = + Verifier.parse_constructor_arguments_for_sourcify_contract(address_hash, smart_contract.abi, deployed_bytecode) do + smart_contract |> __MODULE__.changeset(%{constructor_arguments: args}) |> Repo.update() + %Address{address | smart_contract: %__MODULE__{smart_contract | constructor_arguments: args}} + else + address + end + end - get_implementation_method_abi = - abi - |> Enum.find(fn method -> - Map.get(method, "name") == "getImplementation" && Map.get(method, "stateMutability") == "view" + def check_and_update_constructor_args(other), do: other + + @doc """ + Adds verified metadata from bytecode twin smart-contract to the given smart-contract + """ + @spec add_twin_info_to_contract(map(), Chain.Hash.t(), Chain.Hash.t() | nil) :: map() + def add_twin_info_to_contract(address_result, address_verified_twin_contract, _hash) + when is_nil(address_verified_twin_contract), + do: address_result + + def add_twin_info_to_contract(address_result, address_verified_twin_contract, hash) do + address_verified_twin_contract_updated = + address_verified_twin_contract + |> Map.put(:address_hash, hash) + |> Map.put(:metadata_from_verified_twin, true) + |> Map.put(:implementation_address_hash, nil) + |> Map.put(:implementation_name, nil) + |> Map.put(:implementation_fetched_at, nil) + + address_result + |> Map.put(:smart_contract, address_verified_twin_contract_updated) + end + + @doc """ + Inserts a `t:SmartContract.t/0`. + + As part of inserting a new smart contract, an additional record is inserted for + naming the address for reference. + """ + @spec create_smart_contract(map(), list(), list()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} + def create_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do + new_contract = %__MODULE__{} + + attrs = + attrs + |> Helper.add_contract_code_md5() + + smart_contract_changeset = + new_contract + |> __MODULE__.changeset(attrs) + |> Changeset.put_change(:external_libraries, external_libraries) + + new_contract_additional_source = %SmartContractAdditionalSource{} + + smart_contract_additional_sources_changesets = + if secondary_sources do + secondary_sources + |> Enum.map(fn changeset -> + new_contract_additional_source + |> SmartContractAdditionalSource.changeset(changeset) + end) + else + [] + end + + address_hash = Changeset.get_field(smart_contract_changeset, :address_hash) + + # Enforce ShareLocks tables order (see docs: sharelocks.md) + insert_contract_query = + Multi.new() + |> Multi.run(:set_address_verified, fn repo, _ -> set_address_verified(repo, address_hash) end) + |> Multi.run(:clear_primary_address_names, fn repo, _ -> + AddressName.clear_primary_address_names(repo, address_hash) end) + |> Multi.insert(:smart_contract, smart_contract_changeset) - master_copy_method_abi = - abi - |> Enum.find(fn method -> - Chain.master_copy_pattern?(method) + insert_contract_query_with_additional_sources = + smart_contract_additional_sources_changesets + |> Enum.with_index() + |> Enum.reduce(insert_contract_query, fn {changeset, index}, multi -> + Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset) end) - implementation_address = - cond do - implementation_method_abi -> - get_implementation_address_hash_basic(@implementation_signature, proxy_address_hash, abi) + insert_result = + insert_contract_query_with_additional_sources + |> Repo.transaction() - get_implementation_method_abi -> - get_implementation_address_hash_basic(@get_implementation_signature, proxy_address_hash, abi) + AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) - master_copy_method_abi -> - get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) + case insert_result do + {:ok, %{smart_contract: smart_contract}} -> + {:ok, smart_contract} - true -> - get_implementation_address_hash_eip_1967(proxy_address_hash) + {:error, :smart_contract, changeset, _} -> + {:error, changeset} + + {:error, :set_address_verified, message, _} -> + {:error, message} + end + end + + @doc """ + Updates a `t:SmartContract.t/0`. + + Has the similar logic as create_smart_contract/1. + Used in cases when you need to update row in DB contains SmartContract, e.g. in case of changing + status `partially verified` to `fully verified` (re-verify). + """ + @spec update_smart_contract(map(), list(), list()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} + def update_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do + address_hash = Map.get(attrs, :address_hash) + + query_sources = + from( + source in SmartContractAdditionalSource, + where: source.address_hash == ^address_hash + ) + + _delete_sources = Repo.delete_all(query_sources) + + query = get_smart_contract_query(address_hash) + smart_contract = Repo.one(query) + + smart_contract_changeset = + smart_contract + |> __MODULE__.changeset(attrs) + |> Changeset.put_change(:external_libraries, external_libraries) + + new_contract_additional_source = %SmartContractAdditionalSource{} + + smart_contract_additional_sources_changesets = + if secondary_sources do + secondary_sources + |> Enum.map(fn changeset -> + new_contract_additional_source + |> SmartContractAdditionalSource.changeset(changeset) + end) + else + [] end - save_implementation_data(implementation_address, proxy_address_hash, metadata_from_verified_twin, options) + # Enforce ShareLocks tables order (see docs: sharelocks.md) + insert_contract_query = + Multi.new() + |> Multi.run(:clear_primary_address_names, fn repo, _ -> + AddressName.clear_primary_address_names(repo, address_hash) + end) + |> Multi.update(:smart_contract, smart_contract_changeset) + + insert_contract_query_with_additional_sources = + smart_contract_additional_sources_changesets + |> Enum.with_index() + |> Enum.reduce(insert_contract_query, fn {changeset, index}, multi -> + Multi.insert(multi, "smart_contract_additional_source_#{Integer.to_string(index)}", changeset) + end) + + insert_result = + insert_contract_query_with_additional_sources + |> Repo.transaction() + + AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) + + case insert_result do + {:ok, %{smart_contract: smart_contract}} -> + {:ok, smart_contract} + + {:error, :smart_contract, changeset, _} -> + {:error, changeset} + + {:error, :set_address_verified, message, _} -> + {:error, message} + end end - defp get_implementation_address_hash(_proxy_address_hash, _abi, _, _) do - {nil, nil} + @doc """ + Converts address hash to smart-contract object + """ + @spec address_hash_to_smart_contract_without_twin(Hash.Address.t(), [api?]) :: __MODULE__.t() | nil + def address_hash_to_smart_contract_without_twin(address_hash, options) do + query = get_smart_contract_query(address_hash) + + Chain.select_repo(options).one(query) end - defp get_implementation_address_hash_eip_1967(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + @doc """ + Converts address hash to smart-contract object with metadata_from_verified_twin=true + """ + @spec address_hash_to_smart_contract(Hash.Address.t(), [api?]) :: __MODULE__.t() | nil + def address_hash_to_smart_contract(address_hash, options \\ []) do + current_smart_contract = address_hash_to_smart_contract_without_twin(address_hash, options) + + with true <- is_nil(current_smart_contract), + address_verified_twin_contract = + EIP1167.get_implementation_address(address_hash, options) || + get_address_verified_twin_contract(address_hash, options).verified_contract, + false <- is_nil(address_verified_twin_contract) do + address_verified_twin_contract + |> Map.put(:address_hash, address_hash) + |> Map.put(:metadata_from_verified_twin, true) + |> Map.put(:implementation_address_hash, nil) + |> Map.put(:implementation_name, nil) + |> Map.put(:implementation_fetched_at, nil) + else + _ -> + current_smart_contract + end + end - # https://eips.ethereum.org/EIPS/eip-1967 - storage_slot_logic_contract_address = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + @doc """ + Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the + `t:Explorer.Chain.Address.t/0` with the provided `hash` and `partially_verified` property is not true. - {_status, implementation_address} = - case Contract.eth_get_storage_at_request( - proxy_address_hash, - storage_slot_logic_contract_address, - nil, - json_rpc_named_arguments - ) do - {:ok, empty_address} - when is_burn_signature_or_nil(empty_address) -> - fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) + Returns `true` if found and `false` otherwise. + """ + @spec verified_with_full_match?(Hash.Address.t() | String.t()) :: boolean() + def verified_with_full_match?(address_hash, options \\ []) - {:ok, implementation_logic_address} -> - {:ok, implementation_logic_address} + def verified_with_full_match?(address_hash_str, options) when is_binary(address_hash_str) do + case Chain.string_to_address_hash(address_hash_str) do + {:ok, address_hash} -> + check_verified_with_full_match(address_hash, options) - _ -> - {:ok, nil} - end + _ -> + false + end + end - abi_decode_address_output(implementation_address) + def verified_with_full_match?(address_hash, options) do + check_verified_with_full_match(address_hash, options) end - # changes requested by https://github.com/blockscout/blockscout/issues/4770 - # for support BeaconProxy pattern - defp fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do - # https://eips.ethereum.org/EIPS/eip-1967 - storage_slot_beacon_contract_address = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50" + @doc """ + Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the + `t:Explorer.Chain.Address.t/0` with the provided `hash`. - implementation_method_abi = [ - %{ - "type" => "function", - "stateMutability" => "view", - "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], - "name" => "implementation", - "inputs" => [] - } - ] - - case Contract.eth_get_storage_at_request( - proxy_address_hash, - storage_slot_beacon_contract_address, - nil, - json_rpc_named_arguments - ) do - {:ok, empty_address} - when is_burn_signature_or_nil(empty_address) -> - fetch_openzeppelin_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) - - {:ok, beacon_contract_address} -> - case beacon_contract_address - |> abi_decode_address_output() - |> get_implementation_address_hash_basic(@implementation_signature, implementation_method_abi) do - <> -> - {:ok, implementation_address} - - _ -> - {:ok, beacon_contract_address} - end + Returns `true` if found and `false` otherwise. + """ + @spec verified?(Hash.Address.t() | String.t()) :: boolean() + def verified?(address_hash_str) when is_binary(address_hash_str) do + case Chain.string_to_address_hash(address_hash_str) do + {:ok, address_hash} -> + verified_smart_contract_exists?(address_hash) _ -> - {:ok, nil} + false end end - # changes requested by https://github.com/blockscout/blockscout/issues/5292 - defp fetch_openzeppelin_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do - # This is the keccak-256 hash of "org.zeppelinos.proxy.implementation" - storage_slot_logic_contract_address = "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3" + def verified?(address_hash) do + verified_smart_contract_exists?(address_hash) + end - case Contract.eth_get_storage_at_request( - proxy_address_hash, - storage_slot_logic_contract_address, - nil, - json_rpc_named_arguments - ) do - {:ok, empty_address} - when is_burn_signature(empty_address) -> - {:ok, "0x"} + @doc """ + Checks if it exists a verified `t:Explorer.Chain.SmartContract.t/0` for the + `t:Explorer.Chain.Address.t/0` with the provided `hash`. - {:ok, logic_contract_address} -> - {:ok, logic_contract_address} + Returns `:ok` if found and `:not_found` otherwise. + """ + @spec check_verified_smart_contract_exists(Hash.Address.t()) :: :ok | :not_found + def check_verified_smart_contract_exists(address_hash) do + address_hash + |> verified_smart_contract_exists?() + |> Chain.boolean_to_check_result() + end + @doc """ + Gets smart-contract ABI from the DB for the given address hash of smart-contract + """ + @spec get_smart_contract_abi(String.t(), any()) :: any() + def get_smart_contract_abi(address_hash_string, options \\ []) + + def get_smart_contract_abi(address_hash_string, options) + when not is_nil(address_hash_string) do + with {:ok, implementation_address_hash} <- Chain.string_to_address_hash(address_hash_string), + implementation_smart_contract = + implementation_address_hash + |> address_hash_to_smart_contract(options), + false <- is_nil(implementation_smart_contract) do + implementation_smart_contract + |> Map.get(:abi) + else _ -> - {:ok, nil} + [] end end - defp get_implementation_address_hash_basic(signature, proxy_address_hash, abi) do - implementation_address = - case Reader.query_contract( - proxy_address_hash, - abi, - %{ - "#{signature}" => [] - }, - false - ) do - %{^signature => {:ok, [result]}} -> result - _ -> nil - end + def get_smart_contract_abi(address_hash_string, _) when is_nil(address_hash_string) do + [] + end - address_to_hex(implementation_address) + @doc """ + Gets smart-contract by address hash + """ + @spec get_smart_contract_query(Hash.Address.t() | binary) :: Ecto.Query.t() + def get_smart_contract_query(address_hash) do + from( + smart_contract in __MODULE__, + where: smart_contract.address_hash == ^address_hash + ) end - defp get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + defp upsert_contract_methods(%Changeset{changes: %{abi: abi}} = changeset) do + ContractMethod.upsert_from_abi(abi, get_field(changeset, :address_hash)) - master_copy_storage_pointer = "0x0" + changeset + rescue + exception -> + message = Exception.format(:error, exception, __STACKTRACE__) - {:ok, implementation_address} = - case Contract.eth_get_storage_at_request( - proxy_address_hash, - master_copy_storage_pointer, - nil, - json_rpc_named_arguments - ) do - {:ok, empty_address} - when is_burn_signature(empty_address) -> - {:ok, "0x"} + Logger.error(fn -> ["Error while upserting contract methods: ", message] end) - {:ok, logic_contract_address} -> - {:ok, logic_contract_address} + changeset + end - _ -> - {:ok, nil} - end + defp upsert_contract_methods(changeset), do: changeset + + defp error_message(:compilation), do: error_message_with_log("There was an error compiling your contract.") + + defp error_message(:compiler_version), + do: error_message_with_log("Compiler version does not match, please try again.") + + defp error_message(:generated_bytecode), do: error_message_with_log("Bytecode does not match, please try again.") + + defp error_message(:constructor_arguments), + do: error_message_with_log("Constructor arguments do not match, please try again.") + + defp error_message(:name), do: error_message_with_log("Wrong contract name, please try again.") + defp error_message(:json), do: error_message_with_log("Invalid JSON file.") + + defp error_message(:autodetect_constructor_arguments_failed), + do: + error_message_with_log( + "Autodetection of constructor arguments failed. Please try to input constructor arguments manually." + ) + + defp error_message(:no_creation_data), + do: + error_message_with_log( + "The contract creation transaction has not been indexed yet. Please wait a few minutes and try again." + ) + + defp error_message(:unknown_error), do: error_message_with_log("Unable to verify: unknown error.") + + defp error_message(:deployed_bytecode), + do: error_message_with_log("Deployed bytecode does not correspond to contract creation code.") + + defp error_message(:contract_source_code), do: error_message_with_log("Empty contract source code.") + + defp error_message(string) when is_binary(string), do: error_message_with_log(string) + defp error_message(%{"message" => string} = error) when is_map(error), do: error_message_with_log(string) - abi_decode_address_output(implementation_address) + defp error_message(error) do + Logger.warn(fn -> ["Unknown verifier error: ", inspect(error)] end) + "There was an error validating your contract, please try again." end - defp save_implementation_data(nil, _, _, _), do: {nil, nil} + defp error_message(:compilation, error_message), + do: error_message_with_log("There was an error compiling your contract: #{error_message}") - defp save_implementation_data(empty_address_hash_string, proxy_address_hash, metadata_from_verified_twin, options) - when is_burn_signature_extended(empty_address_hash_string) do - if is_nil(metadata_from_verified_twin) or !metadata_from_verified_twin do - proxy_address_hash - |> Chain.address_hash_to_smart_contract_without_twin(options) - |> changeset(%{ - implementation_name: nil, - implementation_address_hash: nil, - implementation_fetched_at: DateTime.utc_now() - }) - |> Repo.update() - end + defp error_message_with_log(error_string) do + Logger.error("Smart-contract verification error: #{error_string}") + error_string + end - {:empty, :empty} + defp select_error_field(:no_creation_data), do: :address_hash + defp select_error_field(:compiler_version), do: :compiler_version + + defp select_error_field(constructor_arguments) + when constructor_arguments in [:constructor_arguments, :autodetect_constructor_arguments_failed], + do: :constructor_arguments + + defp select_error_field(:name), do: :name + defp select_error_field(_), do: :contract_source_code + + defp to_address_hash(string) when is_binary(string) do + {:ok, address_hash} = Chain.string_to_address_hash(string) + address_hash end - defp save_implementation_data(implementation_address_hash_string, proxy_address_hash, _, options) - when is_binary(implementation_address_hash_string) do - with {:ok, address_hash} <- Chain.string_to_address_hash(implementation_address_hash_string), - proxy_contract <- Chain.address_hash_to_smart_contract_without_twin(proxy_address_hash, options), - false <- is_nil(proxy_contract), - %{implementation: %__MODULE__{name: name}, proxy: proxy_contract} <- %{ - implementation: Chain.address_hash_to_smart_contract(address_hash, options), - proxy: proxy_contract - } do - proxy_contract - |> changeset(%{ - implementation_name: name, - implementation_address_hash: implementation_address_hash_string, - implementation_fetched_at: DateTime.utc_now() - }) - |> Repo.update() + defp to_address_hash(address_hash), do: address_hash - {implementation_address_hash_string, name} - else - %{implementation: _, proxy: proxy_contract} -> - proxy_contract - |> changeset(%{ - implementation_name: nil, - implementation_address_hash: implementation_address_hash_string, - implementation_fetched_at: DateTime.utc_now() - }) - |> Repo.update() + defp db_implementation_data_converter(nil), do: nil + defp db_implementation_data_converter(string) when is_binary(string), do: string + defp db_implementation_data_converter(other), do: to_string(other) - {implementation_address_hash_string, nil} + @doc """ + Function checks by timestamp if new implementation fetching needed + """ + @spec check_implementation_refetch_necessity(Calendar.datetime() | nil) :: boolean() + def check_implementation_refetch_necessity(nil), do: true - true -> - {:ok, address_hash} = Chain.string_to_address_hash(implementation_address_hash_string) - smart_contract = Chain.address_hash_to_smart_contract(address_hash, options) + def check_implementation_refetch_necessity(timestamp) do + if Application.get_env(:explorer, :proxy)[:caching_implementation_data_enabled] do + now = DateTime.utc_now() - {implementation_address_hash_string, smart_contract && smart_contract.name} + fresh_time_distance = get_fresh_time_distance() - _ -> - {implementation_address_hash_string, nil} + timestamp + |> DateTime.add(fresh_time_distance, :millisecond) + |> DateTime.compare(now) != :gt + else + true end end - defp address_to_hex(address) do - if address do - if String.starts_with?(address, "0x") do - address - else - "0x" <> Base.encode16(address, case: :lower) - end + @doc """ + Returns time interval in milliseconds in which fetched proxy info is not needed to be refetched + """ + @spec get_fresh_time_distance() :: integer() + def get_fresh_time_distance do + average_block_time = get_average_block_time_for_implementation_refetch() + + case average_block_time do + 0 -> + Application.get_env(:explorer, :proxy)[:fallback_cached_implementation_data_ttl] + + time -> + round(time) end end - defp abi_decode_address_output(nil), do: nil - - defp abi_decode_address_output("0x"), do: burn_address_hash_string() + defp get_average_block_time_for_implementation_refetch do + if Application.get_env(:explorer, :proxy)[:implementation_data_ttl_via_avg_block_time] do + case AverageBlockTime.average_block_time() do + {:error, :disabled} -> + 0 - defp abi_decode_address_output(address) when is_binary(address) do - if String.length(address) > 42 do - "0x" <> String.slice(address, -40, 40) + duration -> + duration + |> Duration.to_milliseconds() + end else - address + 0 end end - defp abi_decode_address_output(_), do: nil + @spec verified_smart_contract_exists?(Hash.Address.t()) :: boolean() + defp verified_smart_contract_exists?(address_hash) do + query = get_smart_contract_query(address_hash) - @spec select_partially_verified_by_address_hash(binary() | Hash.t(), keyword) :: boolean() | nil - def select_partially_verified_by_address_hash(address_hash, options \\ []) do + Repo.exists?(query) + end + + defp set_address_verified(repo, address_hash) do query = from( - smart_contract in __MODULE__, - where: smart_contract.address_hash == ^address_hash, - select: smart_contract.partially_verified + address in Address, + where: address.hash == ^address_hash ) - Chain.select_repo(options).one(query) + case repo.update_all(query, set: [verified: true]) do + {1, _} -> {:ok, []} + _ -> {:error, "There was an error annotating that the address has been verified."} + end + end + + defp check_verified_with_full_match(address_hash, options) do + smart_contract = address_hash_to_smart_contract_without_twin(address_hash, options) + + if smart_contract, do: !smart_contract.partially_verified, else: false + end + + @spec verified_contracts([ + Chain.paging_options() + | Chain.necessity_by_association_option() + | {:filter, :solidity | :vyper | :yul} + | {:search, String.t()} + | {:sorting, SortingHelper.sorting_params()} + | Chain.api?() + ]) :: [__MODULE__.t()] + def verified_contracts(options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + sorting_options = Keyword.get(options, :sorting, []) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + filter = Keyword.get(options, :filter, nil) + search_string = Keyword.get(options, :search, nil) + + query = from(contract in __MODULE__) + + query + |> filter_contracts(filter) + |> search_contracts(search_string) + |> SortingHelper.apply_sorting(sorting_options, @default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, @default_sorting) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + + defp search_contracts(basic_query, nil), do: basic_query + + defp search_contracts(basic_query, search_string) do + from(contract in basic_query, + where: + ilike(contract.name, ^"%#{search_string}%") or + ilike(fragment("'0x' || encode(?, 'hex')", contract.address_hash), ^"%#{search_string}%") + ) end + + defp filter_contracts(basic_query, :solidity) do + basic_query + |> where(is_vyper_contract: ^false) + end + + defp filter_contracts(basic_query, :vyper) do + basic_query + |> where(is_vyper_contract: ^true) + end + + defp filter_contracts(basic_query, :yul) do + from(query in basic_query, where: is_nil(query.abi)) + end + + defp filter_contracts(basic_query, _), do: basic_query end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex new file mode 100644 index 000000000000..76a6cf63785a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex @@ -0,0 +1,134 @@ +defmodule Explorer.Chain.SmartContract.AuditReport do + @moduledoc """ + The representation of an audit report for a smart contract. + """ + + use Explorer.Schema + + alias Explorer.{Chain, Helper, Repo} + alias Explorer.Chain.Hash + alias Explorer.ThirdPartyIntegrations.AirTable + + @max_reports_per_day_for_contract 5 + + typed_schema "smart_contract_audit_reports" do + field(:address_hash, Hash.Address, null: false) + field(:is_approved, :boolean) + field(:submitter_name, :string, null: false) + field(:submitter_email, :string, null: false) + field(:is_project_owner, :boolean, null: false) + field(:project_name, :string, null: false) + field(:project_url, :string, null: false) + field(:audit_company_name, :string, null: false) + field(:audit_report_url, :string, null: false) + field(:audit_publish_date, :date, null: false) + field(:request_id, :string) + field(:comment, :string) + + timestamps() + end + + @local_fields [:__meta__, :inserted_at, :updated_at, :id, :request_id] + + @doc """ + Returns a map representation of the request. Appends :chain to a resulting map + """ + @spec to_map(__MODULE__.t()) :: map() + def to_map(%__MODULE__{} = request) do + association_fields = request.__struct__.__schema__(:associations) + waste_fields = association_fields ++ @local_fields + + chain = + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] <> + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] + + request |> Map.from_struct() |> Map.drop(waste_fields) |> Map.put(:chain, chain) + end + + @required_fields ~w(address_hash submitter_name submitter_email is_project_owner project_name project_url audit_company_name audit_report_url audit_publish_date)a + @optional_fields ~w(comment is_approved request_id)a + + @max_string_length 255 + @doc """ + Returns a changeset for audit_report. + """ + @spec changeset(struct(), map()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = audit_report, attrs \\ %{}) do + audit_report + |> cast(attrs, @optional_fields ++ @required_fields) + |> validate_required(@required_fields, message: "Required") + |> validate_length(:submitter_email, max: @max_string_length) + |> validate_format(:submitter_email, ~r/^[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}$/i, message: "invalid email address") + |> validate_format(:submitter_name, ~r/[a-zA-Z ]+/i, message: "only letters are allowed") + |> validate_length(:submitter_name, max: @max_string_length) + |> validate_length(:project_name, max: @max_string_length) + |> validate_length(:project_url, max: @max_string_length) + |> validate_length(:audit_company_name, max: @max_string_length) + |> validate_length(:audit_report_url, max: @max_string_length) + |> validate_change(:audit_publish_date, &past_date?/2) + |> validate_change(:audit_report_url, &valid_url?/2) + |> validate_change(:project_url, &valid_url?/2) + |> unique_constraint([:address_hash, :audit_report_url, :audit_publish_date, :audit_company_name], + message: "the report was submitted before", + name: :audit_report_unique_index + ) + |> validate_change(:address_hash, &limit_not_exceeded?/2) + end + + defp past_date?(field, date) do + if Date.compare(Date.utc_today(), date) == :lt do + [{field, "cannot be the future date"}] + else + [] + end + end + + defp valid_url?(field, url) do + if Helper.valid_url?(url) do + [] + else + [{field, "invalid url"}] + end + end + + defp limit_not_exceeded?(field, address_hash) do + if get_reports_count_by_day_for_address_hash_by_day(address_hash) >= @max_reports_per_day_for_contract do + [{field, "max #{@max_reports_per_day_for_contract} reports for address per day"}] + else + [] + end + end + + @doc """ + Insert a new audit report to DB. + """ + @spec create(map()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} + def create(attrs) do + %__MODULE__{} + |> changeset(attrs) + |> AirTable.submit() + |> Repo.insert() + end + + defp get_reports_count_by_day_for_address_hash_by_day(address_hash) do + __MODULE__ + |> where( + [ar], + ar.address_hash == ^address_hash and + fragment("NOW() - ? at time zone 'UTC' <= interval '24 hours'", ar.inserted_at) + ) + |> limit(@max_reports_per_day_for_contract) + |> Repo.aggregate(:count) + end + + @doc """ + Returns a list of audit reports by smart contract address hash. + """ + @spec get_audit_reports_by_smart_contract_address_hash(Hash.Address.t(), keyword()) :: [__MODULE__.t()] + def get_audit_reports_by_smart_contract_address_hash(address_hash, options \\ []) do + __MODULE__ + |> where([ar], ar.address_hash == ^address_hash) + |> where([ar], ar.is_approved == true) + |> Chain.select_repo(options).all() + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/external_library.ex b/apps/explorer/lib/explorer/chain/smart_contract/external_library.ex index ac52388de5bd..62aeb99b7115 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/external_library.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/external_library.ex @@ -3,9 +3,9 @@ defmodule Explorer.Chain.SmartContract.ExternalLibrary do The representation of an external library that was used for a smart contract. """ - use Ecto.Schema + use Explorer.Schema - embedded_schema do + typed_embedded_schema do field(:name) field(:address_hash) end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex new file mode 100644 index 000000000000..672b17ddd65b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex @@ -0,0 +1,302 @@ +defmodule Explorer.Chain.SmartContract.Proxy do + @moduledoc """ + Module for proxy smart-contract implementation detection + """ + + alias EthereumJSONRPC.Contract + alias Explorer.Chain.{Hash, SmartContract} + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.{Basic, EIP1167, EIP1822, EIP1967, EIP930, MasterCopy} + + import Explorer.Chain, + only: [ + string_to_address_hash: 1 + ] + + import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0, is_burn_signature: 1] + + # supported signatures: + # 5c60da1b = keccak256(implementation()) + @implementation_signature "5c60da1b" + # aaf10f42 = keccak256(getImplementation()) + @get_implementation_signature "aaf10f42" + # bb82aa5e = keccak256(comptrollerImplementation()) Compound protocol proxy pattern + @comptroller_implementation_signature "bb82aa5e" + # aaf10f42 = keccak256(getAddress(bytes32)) + @get_address_signature "21f8a721" + + @typep api? :: {:api?, true | false} + + @doc """ + Fetches into DB proxy contract implementation's address and name from different proxy patterns + """ + @spec fetch_implementation_address_hash(Hash.Address.t(), list(), boolean() | nil, [api?]) :: + {String.t() | nil | :empty, String.t() | nil | :empty} + def fetch_implementation_address_hash(proxy_address_hash, proxy_abi, metadata_from_verified_twin, options) + when not is_nil(proxy_address_hash) and not is_nil(proxy_abi) do + implementation_address_hash_string = get_implementation_address_hash_string(proxy_address_hash, proxy_abi) + + SmartContract.save_implementation_data( + implementation_address_hash_string, + proxy_address_hash, + metadata_from_verified_twin, + options + ) + end + + def fetch_implementation_address_hash(_, _, _, _) do + {nil, nil} + end + + @doc """ + Checks if smart-contract is proxy. Returns true/false. + """ + @spec proxy_contract?(SmartContract.t(), any()) :: boolean() + def proxy_contract?(smart_contract, options \\ []) do + {:ok, burn_address_hash} = string_to_address_hash(SmartContract.burn_address_hash_string()) + + if smart_contract.implementation_address_hash && + smart_contract.implementation_address_hash.bytes !== burn_address_hash.bytes do + true + else + {implementation_address_hash_string, _} = SmartContract.get_implementation_address_hash(smart_contract, options) + + with false <- is_nil(implementation_address_hash_string), + {:ok, implementation_address_hash} <- string_to_address_hash(implementation_address_hash_string), + false <- implementation_address_hash.bytes == burn_address_hash.bytes do + true + else + _ -> + false + end + end + end + + @doc """ + Decodes address output into 20 bytes address hash + """ + @spec abi_decode_address_output(any()) :: nil | binary() + def abi_decode_address_output(nil), do: nil + + def abi_decode_address_output("0x"), do: SmartContract.burn_address_hash_string() + + def abi_decode_address_output(address) when is_binary(address) do + if String.length(address) > 42 do + "0x" <> String.slice(address, -40, 40) + else + address + end + end + + def abi_decode_address_output(_), do: nil + + @doc """ + Gets implementation ABI for given proxy smart-contract + """ + @spec get_implementation_abi_from_proxy(any(), any()) :: [map()] + def get_implementation_abi_from_proxy( + %SmartContract{address_hash: proxy_address_hash, abi: abi} = smart_contract, + options + ) + when not is_nil(proxy_address_hash) and not is_nil(abi) do + {implementation_address_hash_string, _name} = SmartContract.get_implementation_address_hash(smart_contract, options) + SmartContract.get_smart_contract_abi(implementation_address_hash_string) + end + + def get_implementation_abi_from_proxy(_, _), do: [] + + @doc """ + Checks if the ABI of the smart-contract follows GnosisSafe proxy pattern + """ + @spec gnosis_safe_contract?([map()]) :: boolean() + def gnosis_safe_contract?(abi) when not is_nil(abi) do + if get_master_copy_pattern(abi), do: true, else: false + end + + def gnosis_safe_contract?(abi) when is_nil(abi), do: false + + @doc """ + Checks if the input of the smart-contract follows master-copy (or Safe) proxy pattern before + fetching its implementation from 0x0 storage pointer + """ + @spec master_copy_pattern?(map()) :: any() + def master_copy_pattern?(method) do + Map.get(method, "type") == "constructor" && + method + |> Enum.find(fn item -> + case item do + {"inputs", inputs} -> + find_input_by_name(inputs, "_masterCopy") || find_input_by_name(inputs, "_singleton") + + _ -> + false + end + end) + end + + @doc """ + Gets implementation from proxy contract's specific storage + """ + @spec get_implementation_from_storage(Hash.Address.t(), String.t(), any()) :: String.t() | nil + def get_implementation_from_storage(proxy_address_hash, storage_slot, json_rpc_named_arguments) do + case Contract.eth_get_storage_at_request( + proxy_address_hash, + storage_slot, + nil, + json_rpc_named_arguments + ) do + {:ok, empty_address_hash_string} + when is_burn_signature(empty_address_hash_string) -> + nil + + {:ok, "0x" <> storage_value} -> + extract_address_hex_from_storage_pointer(storage_value) + + _ -> + nil + end + end + + defp get_implementation_address_hash_string(proxy_address_hash, proxy_abi) do + get_implementation_address_hash_string_eip1167( + proxy_address_hash, + proxy_abi + ) + end + + @doc """ + Returns EIP-1167 implementation address or tries next proxy pattern + """ + @spec get_implementation_address_hash_string_eip1167(Hash.Address.t(), any()) :: String.t() | nil + def get_implementation_address_hash_string_eip1167(proxy_address_hash, proxy_abi) do + get_implementation_address_hash_string_by_module( + EIP1167, + :get_implementation_address_hash_string_eip1967, + [ + proxy_address_hash, + proxy_abi + ] + ) + end + + @doc """ + Returns EIP-1967 implementation address or tries next proxy pattern + """ + @spec get_implementation_address_hash_string_eip1967(Hash.Address.t(), any()) :: String.t() | nil + def get_implementation_address_hash_string_eip1967(proxy_address_hash, proxy_abi) do + get_implementation_address_hash_string_by_module( + EIP1967, + :get_implementation_address_hash_string_eip1822, + [ + proxy_address_hash, + proxy_abi + ] + ) + end + + @doc """ + Returns EIP-1822 implementation address or tries next proxy pattern + """ + @spec get_implementation_address_hash_string_eip1822(Hash.Address.t(), any()) :: String.t() | nil + def get_implementation_address_hash_string_eip1822(proxy_address_hash, proxy_abi) do + get_implementation_address_hash_string_by_module(EIP1822, [proxy_address_hash, proxy_abi]) + end + + defp get_implementation_address_hash_string_by_module( + module, + next_func \\ :fallback_proxy_detection, + [proxy_address_hash, _proxy_abi] = args + ) do + implementation_address_hash_string = module.get_implementation_address_hash_string(proxy_address_hash) + + if !is_nil(implementation_address_hash_string) && implementation_address_hash_string !== burn_address_hash_string() do + implementation_address_hash_string + else + apply(__MODULE__, next_func, args) + end + end + + @spec fallback_proxy_detection(Hash.Address.t(), any()) :: String.t() | nil + def fallback_proxy_detection(proxy_address_hash, proxy_abi) do + implementation_method_abi = get_naive_implementation_abi(proxy_abi, "implementation") + + get_implementation_method_abi = get_naive_implementation_abi(proxy_abi, "getImplementation") + + comptroller_implementation_method_abi = get_naive_implementation_abi(proxy_abi, "comptrollerImplementation") + + master_copy_method_abi = get_master_copy_pattern(proxy_abi) + + get_address_method_abi = get_naive_implementation_abi(proxy_abi, "getAddress") + + cond do + implementation_method_abi -> + Basic.get_implementation_address_hash_string(@implementation_signature, proxy_address_hash, proxy_abi) + + get_implementation_method_abi -> + Basic.get_implementation_address_hash_string(@get_implementation_signature, proxy_address_hash, proxy_abi) + + master_copy_method_abi -> + MasterCopy.get_implementation_address_hash_string(proxy_address_hash) + + comptroller_implementation_method_abi -> + Basic.get_implementation_address_hash_string( + @comptroller_implementation_signature, + proxy_address_hash, + proxy_abi + ) + + get_address_method_abi -> + EIP930.get_implementation_address_hash_string(@get_address_signature, proxy_address_hash, proxy_abi) + + true -> + nil + end + end + + defp get_naive_implementation_abi(abi, getter_name) do + abi + |> Enum.find(fn method -> + Map.get(method, "name") == getter_name && Map.get(method, "stateMutability") == "view" + end) + end + + defp get_master_copy_pattern(abi) do + abi + |> Enum.find(fn method -> + master_copy_pattern?(method) + end) + end + + @doc """ + Returns combined ABI from proxy and implementation smart-contracts + """ + @spec combine_proxy_implementation_abi(any(), any()) :: SmartContract.abi() + def combine_proxy_implementation_abi(smart_contract, options \\ []) + + def combine_proxy_implementation_abi(%SmartContract{abi: abi} = smart_contract, options) when not is_nil(abi) do + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, options) + + if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi + end + + def combine_proxy_implementation_abi(_, _) do + [] + end + + defp find_input_by_name(inputs, name) do + inputs + |> Enum.find(fn input -> + Map.get(input, "name") == name + end) + end + + @doc """ + Decodes 20 bytes address hex from smart-contract storage pointer value + """ + @spec extract_address_hex_from_storage_pointer(binary) :: binary + def extract_address_hex_from_storage_pointer(storage_value) when is_binary(storage_value) do + address_hex = storage_value |> String.slice(-40, 40) |> String.pad_leading(40, ["0"]) + + "0x" <> address_hex + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic.ex new file mode 100644 index 000000000000..dc4f305900ee --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic.ex @@ -0,0 +1,45 @@ +defmodule Explorer.Chain.SmartContract.Proxy.Basic do + @moduledoc """ + Module for fetching proxy implementation from specific smart-contract getter + """ + + alias Explorer.Chain.SmartContract + alias Explorer.SmartContract.Reader + + @doc """ + Gets implementation hash string of proxy contract from getter. + """ + @spec get_implementation_address_hash_string(binary, binary, SmartContract.abi()) :: nil | binary + def get_implementation_address_hash_string(signature, proxy_address_hash, abi) do + implementation_address = + case Reader.query_contract( + proxy_address_hash, + abi, + %{ + "#{signature}" => [] + }, + false + ) do + %{^signature => {:ok, [result]}} -> result + _ -> nil + end + + adds_0x_to_address(implementation_address) + end + + @doc """ + Adds 0x to address at the beginning + """ + @spec adds_0x_to_address(nil | binary()) :: nil | binary() + def adds_0x_to_address(nil), do: nil + + def adds_0x_to_address(address) do + if address do + if String.starts_with?(address, "0x") do + address + else + "0x" <> Base.encode16(address, case: :lower) + end + end + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex new file mode 100644 index 000000000000..acf503c1503e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex @@ -0,0 +1,70 @@ +defmodule Explorer.Chain.SmartContract.Proxy.EIP1167 do + @moduledoc """ + Module for fetching proxy implementation from https://eips.ethereum.org/EIPS/eip-1167 (Minimal Proxy Contract) + """ + + alias Explorer.Chain + alias Explorer.Chain.{Address, Hash, SmartContract} + alias Explorer.Chain.SmartContract.Proxy + + @doc """ + Get implementation address following EIP-1167 + """ + @spec get_implementation_address(Hash.Address.t(), Keyword.t()) :: SmartContract.t() | nil + def get_implementation_address(address_hash, options \\ []) do + address_hash + |> get_implementation_address_hash_string(options) + |> implementation_to_smart_contract(options) + end + + @doc """ + Get implementation address hash string following EIP-1167 + """ + @spec get_implementation_address_hash_string(Hash.Address.t(), Keyword.t()) :: String.t() | nil + def get_implementation_address_hash_string(address_hash, options \\ []) do + case Chain.select_repo(options).get(Address, address_hash) do + nil -> + nil + + target_address -> + contract_code = target_address.contract_code + + case contract_code do + %Chain.Data{bytes: contract_code_bytes} -> + contract_bytecode = Base.encode16(contract_code_bytes, case: :lower) + + contract_bytecode |> get_proxy_eip_1167() |> Proxy.abi_decode_address_output() + + _ -> + nil + end + end + end + + defp get_proxy_eip_1167(contract_bytecode) do + case contract_bytecode do + "363d3d373d3d3d363d73" <> <> <> "5af43d82803e903d91602b57fd5bf3" -> + "0x" <> template_address + + # https://medium.com/coinmonks/the-more-minimal-proxy-5756ae08ee48 + "3d3d3d3d363d3d37363d73" <> <> <> "5af43d3d93803e602a57fd5bf3" -> + "0x" <> template_address + + _ -> + nil + end + end + + defp implementation_to_smart_contract(nil, _options), do: nil + + defp implementation_to_smart_contract(address_hash, options) do + necessity_by_association = %{ + :smart_contract_additional_sources => :optional + } + + address_hash + |> SmartContract.get_smart_contract_query() + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).one(timeout: 10_000) + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex new file mode 100644 index 000000000000..be89f8080f5c --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex @@ -0,0 +1,27 @@ +defmodule Explorer.Chain.SmartContract.Proxy.EIP1822 do + @moduledoc """ + Module for fetching proxy implementation from https://eips.ethereum.org/EIPS/eip-1822 Universal Upgradeable Proxy Standard (UUPS) + """ + alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy + + # keccak256("PROXIABLE") + @storage_slot_proxiable "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" + + @doc """ + Get implementation address hash string following EIP-1822 + """ + @spec get_implementation_address_hash_string(Hash.Address.t()) :: nil | binary + def get_implementation_address_hash_string(proxy_address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + proxiable_contract_address_hash_string = + Proxy.get_implementation_from_storage( + proxy_address_hash, + @storage_slot_proxiable, + json_rpc_named_arguments + ) + + Proxy.abi_decode_address_output(proxiable_contract_address_hash_string) + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex new file mode 100644 index 000000000000..24549c96a132 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex @@ -0,0 +1,102 @@ +defmodule Explorer.Chain.SmartContract.Proxy.EIP1967 do + @moduledoc """ + Module for fetching proxy implementation from https://eips.ethereum.org/EIPS/eip-1967 (Proxy Storage Slots) + """ + alias EthereumJSONRPC.Contract + alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.Basic + + import Explorer.Chain.SmartContract, only: [is_burn_signature: 1] + + # supported signatures: + # 5c60da1b = keccak256(implementation()) + @implementation_signature "5c60da1b" + + @storage_slot_logic_contract_address "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + + # to be precise, it is not the part of the EIP-1967 standard, but still uses the same pattern + # changes requested by https://github.com/blockscout/blockscout/issues/5292 + # This is the keccak-256 hash of "org.zeppelinos.proxy.implementation" + @storage_slot_openzeppelin_contract_address "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3" + + @doc """ + Get implementation address hash string following EIP-1967 + """ + @spec get_implementation_address_hash_string(Hash.Address.t()) :: nil | binary + def get_implementation_address_hash_string(proxy_address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + eip1967_implementation_address_hash_string = + Proxy.get_implementation_from_storage( + proxy_address_hash, + @storage_slot_logic_contract_address, + json_rpc_named_arguments + ) || + fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) + + implementation_address_hash_string = + if eip1967_implementation_address_hash_string do + eip1967_implementation_address_hash_string + else + Proxy.get_implementation_from_storage( + proxy_address_hash, + @storage_slot_openzeppelin_contract_address, + json_rpc_named_arguments + ) + end + + Proxy.abi_decode_address_output(implementation_address_hash_string) + end + + # changes requested by https://github.com/blockscout/blockscout/issues/4770 + # for support BeaconProxy pattern + defp fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do + # https://eips.ethereum.org/EIPS/eip-1967 + storage_slot_beacon_contract_address = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50" + + implementation_method_abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "implementation", + "inputs" => [] + } + ] + + beacon_contract_address = + case Contract.eth_get_storage_at_request( + proxy_address_hash, + storage_slot_beacon_contract_address, + nil, + json_rpc_named_arguments + ) do + {:ok, empty_address} + when is_burn_signature(empty_address) -> + nil + + {:ok, "0x" <> storage_value} -> + Proxy.extract_address_hex_from_storage_pointer(storage_value) + + _ -> + nil + end + + if beacon_contract_address do + case @implementation_signature + |> Basic.get_implementation_address_hash_string( + beacon_contract_address, + implementation_method_abi + ) do + <> -> + implementation_address + + _ -> + nil + end + else + nil + end + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_930.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_930.ex new file mode 100644 index 000000000000..600128191aed --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_930.ex @@ -0,0 +1,32 @@ +defmodule Explorer.Chain.SmartContract.Proxy.EIP930 do + @moduledoc """ + Module for fetching proxy implementation from smart-contract getter following https://github.com/ethereum/EIPs/issues/930 + """ + + alias Explorer.Chain.SmartContract + alias Explorer.Chain.SmartContract.Proxy.Basic + alias Explorer.SmartContract.Reader + + @storage_slot_logic_contract_address "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + + @doc """ + Gets implementation hash string of proxy contract from getter. + """ + @spec get_implementation_address_hash_string(binary, binary, SmartContract.abi()) :: nil | binary + def get_implementation_address_hash_string(signature, proxy_address_hash, abi) do + implementation_address = + case Reader.query_contract( + proxy_address_hash, + abi, + %{ + "#{signature}" => [@storage_slot_logic_contract_address] + }, + false + ) do + %{^signature => {:ok, [result]}} -> result + _ -> nil + end + + Basic.adds_0x_to_address(implementation_address) + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex new file mode 100644 index 000000000000..ce5a7aed49aa --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex @@ -0,0 +1,42 @@ +defmodule Explorer.Chain.SmartContract.Proxy.MasterCopy do + @moduledoc """ + Module for fetching master-copy proxy implementation + """ + + alias EthereumJSONRPC.Contract + alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy + + import Explorer.Chain.SmartContract, only: [is_burn_signature: 1] + + @doc """ + Gets implementation address hash string for proxy contract from master-copy pattern + """ + @spec get_implementation_address_hash_string(Hash.Address.t()) :: nil | binary + def get_implementation_address_hash_string(proxy_address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + master_copy_storage_pointer = "0x0" + + {:ok, implementation_address} = + case Contract.eth_get_storage_at_request( + proxy_address_hash, + master_copy_storage_pointer, + nil, + json_rpc_named_arguments + ) do + {:ok, empty_address} + when is_burn_signature(empty_address) -> + {:ok, "0x"} + + {:ok, "0x" <> storage_value} -> + logic_contract_address = Proxy.extract_address_hex_from_storage_pointer(storage_value) + {:ok, logic_contract_address} + + _ -> + {:ok, nil} + end + + Proxy.abi_decode_address_output(implementation_address) + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex new file mode 100644 index 000000000000..b952ee1373aa --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex @@ -0,0 +1,118 @@ +defmodule Explorer.Chain.SmartContract.Proxy.VerificationStatus do + @moduledoc """ + Represents single proxy verification submission + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Explorer.Chain.Hash + alias Explorer.{Chain, Repo} + + @typep status :: integer() | atom() + + @typedoc """ + * `contract_address_hash` - address of the contract which was tried to verify + * `status` - submission status: :pending | :pass | :fail + * `uid` - unique verification identifier + """ + @primary_key false + typed_schema "proxy_smart_contract_verification_statuses" do + field(:uid, :string, primary_key: true, null: false) + field(:status, Ecto.Enum, values: [pending: 0, pass: 1, fail: 2], null: false) + field(:contract_address_hash, Hash.Address, null: false) + + timestamps() + end + + @required_fields ~w(uid status contract_address_hash)a + + @doc """ + Creates a changeset based on the `struct` and `params`. + """ + @spec changeset(Explorer.Chain.SmartContract.Proxy.VerificationStatus.t()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = struct, params \\ %{}) do + struct + |> cast(params, @required_fields) + |> validate_required(@required_fields) + end + + @doc """ + Inserts verification status + """ + @spec insert_status(String.t(), status(), Hash.Address.t() | String.t()) :: any() + def insert_status(uid, status, address_hash) do + {:ok, hash} = if is_binary(address_hash), do: Chain.string_to_address_hash(address_hash), else: {:ok, address_hash} + + %__MODULE__{} + |> changeset(%{uid: uid, status: status, contract_address_hash: hash}) + |> Repo.insert() + end + + @doc """ + Updates verification status + """ + @spec update_status(String.t(), status()) :: __MODULE__.t() + def update_status(uid, status) do + __MODULE__ + |> Repo.get_by(uid: uid) + |> changeset(%{status: status}) + |> Repo.update() + end + + @doc """ + Fetches verification status + """ + @spec fetch_status(binary()) :: __MODULE__.t() | nil + def fetch_status(uid) do + case validate_uid(uid) do + {:ok, valid_uid} -> + __MODULE__ + |> Repo.get_by(uid: valid_uid) + + _ -> + nil + end + end + + @doc """ + Generates uid based on address hash and timestamp + """ + @spec generate_uid(Explorer.Chain.Hash.t()) :: String.t() + def generate_uid(%Hash{byte_count: 20, bytes: address_hash}) do + address_encoded = Base.encode16(address_hash, case: :lower) + timestamp = DateTime.utc_now() |> DateTime.to_unix() |> Integer.to_string(16) |> String.downcase() + address_encoded <> timestamp + end + + @doc """ + Validates uid + """ + @spec validate_uid(String.t()) :: :error | {:ok, <<_::64, _::_*8>>} + def validate_uid(<<_address::binary-size(40), timestamp_hex::binary>> = uid) do + case Integer.parse(timestamp_hex, 16) do + {timestamp, ""} -> + if DateTime.utc_now() |> DateTime.to_unix() > timestamp do + {:ok, uid} + else + :error + end + + _ -> + :error + end + end + + def validate_uid(_), do: :error + + @doc """ + Sets proxy verification result + """ + @spec set_proxy_verification_result({String.t() | nil | :empty, String.t() | nil | :empty}, String.t()) :: + __MODULE__.t() + def set_proxy_verification_result({empty_or_nil, _}, uid) when empty_or_nil in [:empty, nil], + do: update_status(uid, :fail) + + def set_proxy_verification_result({_, _}, uid), do: update_status(uid, :pass) +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex b/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex index 0571238c872e..83037584c1a4 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex @@ -12,21 +12,14 @@ defmodule Explorer.Chain.SmartContract.VerificationStatus do @typedoc """ * `address_hash` - address of the contract which was tried to verify - * `status` - try status: :pending | :pass | :fail + * `status` - try status: :pending | :pass | :fail * `uid` - unique verification try identifier """ - - @type t :: %__MODULE__{ - uid: String.t(), - address_hash: Hash.Address.t(), - status: non_neg_integer() - } - @primary_key false - schema "contract_verification_status" do - field(:uid, :string, primary_key: true) - field(:status, :integer) - field(:address_hash, Hash.Address) + typed_schema "contract_verification_status" do + field(:uid, :string, primary_key: true, null: false) + field(:status, :integer, null: false) + field(:address_hash, Hash.Address, null: false) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/smart_contract_additional_sources.ex b/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex similarity index 63% rename from apps/explorer/lib/explorer/chain/smart_contract_additional_sources.ex rename to apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex index 850c28f4b678..cf3b4487ccdd 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract_additional_sources.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex @@ -8,28 +8,26 @@ defmodule Explorer.Chain.SmartContractAdditionalSource do use Explorer.Schema + import Explorer.Chain, only: [select_repo: 1] + alias Explorer.Chain.{Hash, SmartContract} @typedoc """ * `file_name` - the name of the Solidity file with contract code (with extension). * `contract_source_code` - the Solidity source code from the file with `file_name`. + * `address_hash` - foreign key for `smart_contract`. """ - - @type t :: %Explorer.Chain.SmartContractAdditionalSource{ - file_name: String.t(), - contract_source_code: String.t() - } - - schema "smart_contracts_additional_sources" do - field(:file_name, :string) - field(:contract_source_code, :string) + typed_schema "smart_contracts_additional_sources" do + field(:file_name, :string, null: false) + field(:contract_source_code, :string, null: false) belongs_to( :smart_contract, SmartContract, foreign_key: :address_hash, references: :address_hash, - type: Hash.Address + type: Hash.Address, + null: false ) timestamps() @@ -59,5 +57,24 @@ defmodule Explorer.Chain.SmartContractAdditionalSource do add_error(validated, :contract_source_code, error_message(error)) end + @doc """ + Returns all additional sources for the given smart-contract address hash + """ + @spec get_contract_additional_sources(SmartContract.t() | nil, Keyword.t()) :: [__MODULE__.t()] + def get_contract_additional_sources(smart_contract, options) do + if smart_contract do + all_additional_sources_query = + from( + s in __MODULE__, + where: s.address_hash == ^smart_contract.address_hash + ) + + all_additional_sources_query + |> select_repo(options).all() + else + [] + end + end + defp error_message(_), do: "There was an error validating your contract, please try again." end diff --git a/apps/explorer/lib/explorer/chain/stability/validator.ex b/apps/explorer/lib/explorer/chain/stability/validator.ex new file mode 100644 index 000000000000..309d95814bd8 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/stability/validator.ex @@ -0,0 +1,288 @@ +defmodule Explorer.Chain.Stability.Validator do + @moduledoc """ + Stability validators + """ + + use Explorer.Schema + + alias Explorer.Chain.{Address, Import} + alias Explorer.Chain.Hash.Address, as: HashAddress + alias Explorer.{Chain, Repo, SortingHelper} + alias Explorer.SmartContract.Reader + + require Logger + + @default_sorting [ + asc: :state, + asc: :address_hash + ] + + @state_enum [active: 0, probation: 1, inactive: 2] + + @primary_key false + typed_schema "validators_stability" do + field(:address_hash, HashAddress, primary_key: true) + field(:state, Ecto.Enum, values: @state_enum) + field(:blocks_validated, :integer, virtual: true) + + has_one(:address, Address, foreign_key: :hash, references: :address_hash) + timestamps() + end + + @required_attrs ~w(address_hash)a + @optional_attrs ~w(state)a + def changeset(%__MODULE__{} = validator, attrs) do + validator + |> cast(attrs, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:address_hash) + end + + @doc """ + Get validators list. + Keyword could contain: + - paging_options + - necessity_by_association + - sorting (supported by `Explorer.SortingHelper` module) + - state (one of `@state_enum`) + """ + @spec get_paginated_validators(keyword()) :: [t()] + def get_paginated_validators(options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + sorting = Keyword.get(options, :sorting, []) + states = Keyword.get(options, :state, []) + + __MODULE__ + |> apply_filter_by_state(states) + |> select_merge([vs], %{ + blocks_validated: + fragment( + "SELECT count(*) FROM blocks WHERE miner_hash = ?", + vs.address_hash + ) + }) + |> Chain.join_associations(necessity_by_association) + |> SortingHelper.apply_sorting(sorting, @default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) + |> Chain.select_repo(options).all() + end + + defp apply_filter_by_state(query, []), do: query + + defp apply_filter_by_state(query, states) do + query + |> where([vs], vs.state in ^states) + end + + @doc """ + Get all validators + """ + @spec get_all_validators(keyword()) :: [t()] + def get_all_validators(options \\ []) do + __MODULE__ + |> Chain.select_repo(options).all() + end + + @get_active_validator_list_abi %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}], + "name" => "getActiveValidatorList", + "inputs" => [] + } + + @get_validator_list_abi %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}], + "name" => "getValidatorList", + "inputs" => [] + } + + @get_validator_missing_blocks_abi %{ + "inputs" => [ + %{ + "internalType" => "address", + "name" => "validator", + "type" => "address" + } + ], + "name" => "getValidatorMissingBlocks", + "outputs" => [ + %{ + "internalType" => "uint256", + "name" => "", + "type" => "uint256" + } + ], + "stateMutability" => "view", + "type" => "function" + } + + @get_active_validator_list_method_id "a5aa7380" + @get_validator_list_method_id "e35c0f7d" + @get_validator_missing_blocks_method_id "41ee9a53" + + @stability_validator_controller_contract "0x0000000000000000000000000000000000000805" + + @doc """ + Do batch eth_call of `getValidatorList` and `getActiveValidatorList` methods to `@stability_validator_controller_contract`. + Returns a map with two lists: `active` and `all`, or nil if error. + """ + @spec fetch_validators_lists :: nil | %{active: list(binary()), all: list(binary())} + def fetch_validators_lists do + abi = [@get_active_validator_list_abi, @get_validator_list_abi] + params = %{@get_validator_list_method_id => [], @get_active_validator_list_method_id => []} + + case Reader.query_contract(@stability_validator_controller_contract, abi, params, false) do + %{ + @get_active_validator_list_method_id => {:ok, [active_validators_list]}, + @get_validator_list_method_id => {:ok, [validators_list]} + } -> + %{active: active_validators_list, all: validators_list} + + error -> + Logger.warn(fn -> ["Error on getting validator lists: #{inspect(error)}"] end) + nil + end + end + + @doc """ + Do batch eth_call of `getValidatorMissingBlocks` method to #{@stability_validator_controller_contract}. + Accept: list of validator address hashes + Returns a map: validator_address_hash => missing_blocks_number + """ + @spec fetch_missing_blocks_numbers(list(binary())) :: map() + def fetch_missing_blocks_numbers(validators_address_hashes) do + validators_address_hashes + |> Enum.map(&format_request_missing_blocks_number/1) + |> Reader.query_contracts([@get_validator_missing_blocks_abi]) + |> Enum.zip_reduce(validators_address_hashes, %{}, fn response, address_hash, acc -> + result = + case format_missing_blocks_result(response) do + {:error, message} -> + Logger.warn(fn -> ["Error on getValidatorMissingBlocks for #{validators_address_hashes}: #{message}"] end) + nil + + amount -> + amount + end + + Map.put(acc, address_hash, result) + end) + end + + defp format_missing_blocks_result({:ok, [amount]}) do + amount + end + + defp format_missing_blocks_result({:error, error_message}) do + {:error, error_message} + end + + defp format_request_missing_blocks_number(address_hash) do + %{ + contract_address: @stability_validator_controller_contract, + method_id: @get_validator_missing_blocks_method_id, + args: [address_hash] + } + end + + @doc """ + Convert missing block number to state + """ + @spec missing_block_number_to_state(integer()) :: atom() + def missing_block_number_to_state(integer) when integer > 0, do: :probation + def missing_block_number_to_state(integer) when integer == 0, do: :active + def missing_block_number_to_state(_), do: nil + + @doc """ + Delete validators by address hashes + """ + @spec delete_validators_by_address_hashes([binary() | HashAddress.t()]) :: {non_neg_integer(), nil | []} | :ignore + def delete_validators_by_address_hashes(list) when is_list(list) and length(list) > 0 do + __MODULE__ + |> where([vs], vs.address_hash in ^list) + |> Repo.delete_all() + end + + def delete_validators_by_address_hashes(_), do: :ignore + + @doc """ + Insert validators + """ + @spec insert_validators([map()]) :: {non_neg_integer(), nil | []} + def insert_validators(validators) do + Repo.insert_all(__MODULE__, validators, + on_conflict: {:replace_all_except, [:inserted_at]}, + conflict_target: [:address_hash] + ) + end + + @doc """ + Append timestamps (:inserted_at, :updated_at) + """ + @spec append_timestamps(map()) :: map() + def append_timestamps(validator) do + Map.merge(validator, Import.timestamps()) + end + + @doc """ + Derive next page params from %Explorer.Chain.Stability.Validator{} + """ + @spec next_page_params(t()) :: map() + def next_page_params(%__MODULE__{state: state, address_hash: address_hash, blocks_validated: blocks_validated}) do + %{"state" => state, "address_hash" => address_hash, "blocks_validated" => blocks_validated} + end + + @doc """ + Returns state enum + """ + @spec state_enum() :: Keyword.t() + def state_enum, do: @state_enum + + @doc """ + Returns dynamic query for validated blocks count. Needed for SortingHelper + """ + @spec dynamic_validated_blocks() :: Ecto.Query.dynamic_expr() + def dynamic_validated_blocks do + dynamic( + [vs], + fragment( + "SELECT count(*) FROM blocks WHERE miner_hash = ?", + vs.address_hash + ) + ) + end + + @doc """ + Returns total count of validators. + """ + @spec count_validators() :: integer() + def count_validators do + Repo.aggregate(__MODULE__, :count, :address_hash) + end + + @doc """ + Returns count of new validators (inserted withing last 24h). + """ + @spec count_new_validators() :: integer() + def count_new_validators do + __MODULE__ + |> where([vs], vs.inserted_at >= ago(1, "day")) + |> Repo.aggregate(:count, :address_hash) + end + + @doc """ + Returns count of active validators. + """ + @spec count_active_validators() :: integer() + def count_active_validators do + __MODULE__ + |> where([vs], vs.state == :active) + |> Repo.aggregate(:count, :address_hash) + end +end diff --git a/apps/explorer/lib/explorer/chain/supply.ex b/apps/explorer/lib/explorer/chain/supply.ex index b442e4012597..0287a0dde80b 100644 --- a/apps/explorer/lib/explorer/chain/supply.ex +++ b/apps/explorer/lib/explorer/chain/supply.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Supply do """ @doc """ - The current total number of coins minted minus verifiably burned coins. + The current total number of coins minted minus verifiably burnt coins. """ @callback total :: non_neg_integer() | %Decimal{sign: 1} diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index fc12af368a0d..13bcb12d3ccf 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -1,3 +1,55 @@ +defmodule Explorer.Chain.Token.Schema do + @moduledoc false + + alias Explorer.Chain.{Address, Hash} + + if Application.compile_env(:explorer, Explorer.Chain.BridgedToken)[:enabled] do + @bridged_field [ + quote do + field(:bridged, :boolean) + end + ] + else + @bridged_field [] + end + + defmacro generate do + quote do + @primary_key false + typed_schema "tokens" do + field(:name, :string) + field(:symbol, :string) + field(:total_supply, :decimal) + field(:decimals, :decimal) + field(:type, :string, null: false) + field(:cataloged, :boolean) + field(:holder_count, :integer) + field(:skip_metadata, :boolean) + field(:total_supply_updated_at_block, :integer) + field(:fiat_value, :decimal) + field(:circulating_market_cap, :decimal) + field(:icon_url, :string) + field(:is_verified_via_admin_panel, :boolean) + field(:volume_24h, :decimal) + + belongs_to( + :contract_address, + Address, + foreign_key: :contract_address_hash, + primary_key: true, + references: :hash, + type: Hash.Address, + null: false + ) + + unquote_splicing(@bridged_field) + + timestamps() + end + end + end +end + defmodule Explorer.Chain.Token do @moduledoc """ Represents a token. @@ -9,6 +61,7 @@ defmodule Explorer.Chain.Token do * ERC-20 * ERC-721 * ERC-1155 + * ERC-404 ## Token Specifications @@ -16,50 +69,27 @@ defmodule Explorer.Chain.Token do * [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) * [ERC-777](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-777.md) * [ERC-1155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md) + * [ERC-404](https://github.com/Pandora-Labs-Org/erc404) """ use Explorer.Schema + require Explorer.Chain.Token.Schema + import Ecto.{Changeset, Query} alias Ecto.Changeset - alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{Address, Hash, Token} + alias Explorer.{Chain, SortingHelper} + alias Explorer.Chain.{BridgedToken, Hash, Search, Token} alias Explorer.SmartContract.Helper - @typedoc """ - * `name` - Name of the token - * `symbol` - Trading symbol of the token - * `total_supply` - The total supply of the token - * `decimals` - Number of decimal places the token can be subdivided to - * `type` - Type of token - * `cataloged` - Flag for if token information has been cataloged - * `contract_address` - The `t:Address.t/0` of the token's contract - * `contract_address_hash` - Address hash foreign key - * `holder_count` - the number of `t:Explorer.Chain.Address.t/0` (except the burn address) that have a - `t:Explorer.Chain.CurrentTokenBalance.t/0` `value > 0`. Can be `nil` when data not migrated. - * `fiat_value` - The price of a token in a configured currency (USD by default). - * `circulating_market_cap` - The circulating market cap of a token in a configured currency (USD by default). - * `icon_url` - URL of the token's icon. - * `is_verified_via_admin_panel` - is token verified via admin panel. - """ - @type t :: %Token{ - name: String.t(), - symbol: String.t(), - total_supply: Decimal.t() | nil, - decimals: non_neg_integer(), - type: String.t(), - cataloged: boolean(), - contract_address: %Ecto.Association.NotLoaded{} | Address.t(), - contract_address_hash: Hash.Address.t(), - holder_count: non_neg_integer() | nil, - skip_metadata: boolean(), - total_supply_updated_at_block: non_neg_integer() | nil, - fiat_value: Decimal.t() | nil, - circulating_market_cap: Decimal.t() | nil, - icon_url: String.t(), - is_verified_via_admin_panel: boolean() - } + @default_sorting [ + desc_nulls_last: :circulating_market_cap, + desc_nulls_last: :fiat_value, + desc_nulls_last: :holder_count, + asc: :name, + asc: :contract_address_hash + ] @derive {Poison.Encoder, except: [ @@ -77,41 +107,33 @@ defmodule Explorer.Chain.Token do :updated_at ]} - @primary_key false - schema "tokens" do - field(:name, :string) - field(:symbol, :string) - field(:total_supply, :decimal) - field(:decimals, :decimal) - field(:type, :string) - field(:cataloged, :boolean) - field(:holder_count, :integer) - field(:skip_metadata, :boolean) - field(:total_supply_updated_at_block, :integer) - field(:fiat_value, :decimal) - field(:circulating_market_cap, :decimal) - field(:icon_url, :string) - field(:is_verified_via_admin_panel, :boolean) - - belongs_to( - :contract_address, - Address, - foreign_key: :contract_address_hash, - primary_key: true, - references: :hash, - type: Hash.Address - ) - - timestamps() - end + @typedoc """ + * `name` - Name of the token + * `symbol` - Trading symbol of the token + * `total_supply` - The total supply of the token + * `decimals` - Number of decimal places the token can be subdivided to + * `type` - Type of token + * `cataloged` - Flag for if token information has been cataloged + * `contract_address` - The `t:Address.t/0` of the token's contract + * `contract_address_hash` - Address hash foreign key + * `holder_count` - the number of `t:Explorer.Chain.Address.t/0` (except the burn address) that have a + `t:Explorer.Chain.CurrentTokenBalance.t/0` `value > 0`. Can be `nil` when data not migrated. + * `fiat_value` - The price of a token in a configured currency (USD by default). + * `circulating_market_cap` - The circulating market cap of a token in a configured currency (USD by default). + * `icon_url` - URL of the token's icon. + * `is_verified_via_admin_panel` - is token verified via admin panel. + """ + Explorer.Chain.Token.Schema.generate() @required_attrs ~w(contract_address_hash type)a - @optional_attrs ~w(cataloged decimals name symbol total_supply skip_metadata total_supply_updated_at_block updated_at fiat_value circulating_market_cap icon_url is_verified_via_admin_panel)a + @optional_attrs ~w(cataloged decimals name symbol total_supply skip_metadata total_supply_updated_at_block updated_at fiat_value circulating_market_cap icon_url is_verified_via_admin_panel volume_24h)a @doc false def changeset(%Token{} = token, params \\ %{}) do + additional_attrs = if BridgedToken.enabled?(), do: [:bridged], else: [] + token - |> cast(params, @required_attrs ++ @optional_attrs) + |> cast(params, @required_attrs ++ @optional_attrs ++ additional_attrs) |> validate_required(@required_attrs) |> trim_name() |> sanitize_token_input(:name) @@ -163,178 +185,68 @@ defmodule Explorer.Chain.Token do def base_token_query(type, sorting) do query = from(t in Token, preload: [:contract_address]) - query |> apply_filter(type) |> apply_sorting(sorting) - end - - defp apply_filter(query, empty_type) when empty_type in [nil, []], do: query - - defp apply_filter(query, token_types) when is_list(token_types) do - from(t in query, where: t.type in ^token_types) - end - - @default_sorting [ - desc_nulls_last: :circulating_market_cap, - desc_nulls_last: :holder_count, - asc: :name, - asc: :contract_address_hash - ] - - defp apply_sorting(query, sorting) when is_list(sorting) do - from(t in query, order_by: ^sorting_with_defaults(sorting)) - end - - defp sorting_with_defaults(sorting) when is_list(sorting) do - (sorting ++ @default_sorting) - |> Enum.uniq_by(fn {_, field} -> field end) - end - - def page_tokens(query, paging_options, sorting \\ []) - def page_tokens(query, %PagingOptions{key: nil}, _sorting), do: query - - def page_tokens( - query, - %PagingOptions{ - key: %{} = key - }, - sorting - ) do - dynamic_where = sorting |> sorting_with_defaults() |> do_page_tokens() - - from(token in query, - where: ^dynamic_where.(key) - ) - end - - defp do_page_tokens([{order, column} | rest]) do - fn key -> page_tokens_by_column(key, column, order, do_page_tokens(rest)) end - end - - defp do_page_tokens([]), do: nil - - defp page_tokens_by_column(%{fiat_value: nil} = key, :fiat_value, :desc_nulls_last, next_column) do - dynamic( - [t], - is_nil(t.fiat_value) and ^next_column.(key) - ) - end - - defp page_tokens_by_column(%{fiat_value: nil} = key, :fiat_value, :asc_nulls_first, next_column) do - next_column.(key) - end - - defp page_tokens_by_column(%{fiat_value: fiat_value} = key, :fiat_value, :desc_nulls_last, next_column) do - dynamic( - [t], - is_nil(t.fiat_value) or t.fiat_value < ^fiat_value or - (t.fiat_value == ^fiat_value and ^next_column.(key)) - ) - end - - defp page_tokens_by_column(%{fiat_value: fiat_value} = key, :fiat_value, :asc_nulls_first, next_column) do - dynamic( - [t], - not is_nil(t.fiat_value) and - (t.fiat_value > ^fiat_value or - (t.fiat_value == ^fiat_value and ^next_column.(key))) - ) - end - - defp page_tokens_by_column( - %{circulating_market_cap: nil} = key, - :circulating_market_cap, - :desc_nulls_last, - next_column - ) do - dynamic( - [t], - is_nil(t.circulating_market_cap) and ^next_column.(key) - ) + query |> apply_filter(type) |> SortingHelper.apply_sorting(sorting, @default_sorting) end - defp page_tokens_by_column( - %{circulating_market_cap: nil} = key, - :circulating_market_cap, - :asc_nulls_first, - next_column - ) do - next_column.(key) - end + def default_sorting, do: @default_sorting - defp page_tokens_by_column( - %{circulating_market_cap: circulating_market_cap} = key, - :circulating_market_cap, - :desc_nulls_last, - next_column - ) do - dynamic( - [t], - is_nil(t.circulating_market_cap) or t.circulating_market_cap < ^circulating_market_cap or - (t.circulating_market_cap == ^circulating_market_cap and ^next_column.(key)) - ) - end - - defp page_tokens_by_column( - %{circulating_market_cap: circulating_market_cap} = key, - :circulating_market_cap, - :asc_nulls_first, - next_column - ) do - dynamic( - [t], - not is_nil(t.circulating_market_cap) and - (t.circulating_market_cap > ^circulating_market_cap or - (t.circulating_market_cap == ^circulating_market_cap and ^next_column.(key))) - ) - end + @doc """ + Lists the top `t:__MODULE__.t/0`'s'. + """ + @spec list_top(String.t() | nil, [ + Chain.paging_options() + | {:sorting, SortingHelper.sorting_params()} + | {:token_type, [String.t()]} + ]) :: [Token.t()] + def list_top(filter, options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + token_type = Keyword.get(options, :token_type, nil) + sorting = Keyword.get(options, :sorting, []) - defp page_tokens_by_column(%{holder_count: nil} = key, :holder_count, :desc_nulls_last, next_column) do - dynamic( - [t], - is_nil(t.holder_count) and ^next_column.(key) - ) - end + query = from(t in Token, preload: [:contract_address]) - defp page_tokens_by_column(%{holder_count: nil} = key, :holder_count, :asc_nulls_first, next_column) do - next_column.(key) - end + sorted_paginated_query = + query + |> apply_filter(token_type) + |> SortingHelper.apply_sorting(sorting, @default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) - defp page_tokens_by_column(%{holder_count: holder_count} = key, :holder_count, :desc_nulls_last, next_column) do - dynamic( - [t], - is_nil(t.holder_count) or t.holder_count < ^holder_count or - (t.holder_count == ^holder_count and ^next_column.(key)) - ) - end + filtered_query = + case filter && filter !== "" && Search.prepare_search_term(filter) do + {:some, filter_term} -> + sorted_paginated_query + |> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term)) - defp page_tokens_by_column(%{holder_count: holder_count} = key, :holder_count, :asc_nulls_first, next_column) do - dynamic( - [t], - not is_nil(t.holder_count) and - (t.holder_count > ^holder_count or - (t.holder_count == ^holder_count and ^next_column.(key))) - ) - end + _ -> + sorted_paginated_query + end - defp page_tokens_by_column(%{name: nil} = key, :name, :asc, next_column) do - dynamic( - [t], - is_nil(t.name) and ^next_column.(key) - ) + filtered_query + |> Chain.select_repo(options).all() end - defp page_tokens_by_column(%{name: name} = key, :name, :asc, next_column) do - dynamic( - [t], - is_nil(t.name) or - (t.name > ^name or (t.name == ^name and ^next_column.(key))) - ) - end + defp apply_filter(query, empty_type) when empty_type in [nil, []], do: query - defp page_tokens_by_column(%{contract_address_hash: contract_address_hash}, :contract_address_hash, :asc, nil) do - dynamic([t], t.contract_address_hash > ^contract_address_hash) + defp apply_filter(query, token_types) when is_list(token_types) do + from(t in query, where: t.type in ^token_types) end def get_by_contract_address_hash(hash, options) do Chain.select_repo(options).get_by(__MODULE__, contract_address_hash: hash) end + + @doc """ + For usage in Indexer.Fetcher.TokenInstance.LegacySanitizeERC721 + """ + @spec ordered_erc_721_token_address_hashes_list_query(integer(), Hash.Address.t() | nil) :: Ecto.Query.t() + def ordered_erc_721_token_address_hashes_list_query(limit, last_address_hash \\ nil) do + query = + __MODULE__ + |> order_by([token], asc: token.contract_address_hash) + |> where([token], token.type == "ERC-721") + |> limit(^limit) + |> select([token], token.contract_address_hash) + + (last_address_hash && where(query, [token], token.contract_address_hash > ^last_address_hash)) || query + end end diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index c1b5bcd5f4ff..3807d3b88bf7 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -1,11 +1,13 @@ defmodule Explorer.Chain.Token.Instance do @moduledoc """ - Represents an ERC-721/ERC-1155 token instance and stores metadata defined in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md. + Represents an ERC-721/ERC-1155/ERC-404 token instance and stores metadata defined in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md. """ use Explorer.Schema - alias Explorer.Chain.{Address, Block, Hash, Token, TokenTransfer} + alias Explorer.{Chain, Helper} + alias Explorer.Chain.{Address, Hash, Token, TokenTransfer} + alias Explorer.Chain.Address.CurrentTokenBalance alias Explorer.Chain.Token.Instance alias Explorer.PagingOptions @@ -15,24 +17,15 @@ defmodule Explorer.Chain.Token.Instance do * `metadata` - Token instance metadata * `error` - error fetching token instance """ - - @type t :: %Instance{ - token_id: non_neg_integer(), - token_contract_address_hash: Hash.Address.t(), - metadata: map() | nil, - error: String.t(), - owner_address_hash: Hash.Address.t(), - owner_updated_at_block: Block.block_number(), - owner_updated_at_log_index: non_neg_integer() - } - @primary_key false - schema "token_instances" do - field(:token_id, :decimal, primary_key: true) + typed_schema "token_instances" do + field(:token_id, :decimal, primary_key: true, null: false) field(:metadata, :map) field(:error, :string) field(:owner_updated_at_block, :integer) field(:owner_updated_at_log_index, :integer) + field(:current_token_balance, :any, virtual: true) + field(:is_unique, :boolean, virtual: true) belongs_to(:owner, Address, foreign_key: :owner_address_hash, references: :hash, type: Hash.Address) @@ -42,7 +35,8 @@ defmodule Explorer.Chain.Token.Instance do foreign_key: :token_contract_address_hash, references: :contract_address_hash, type: Hash.Address, - primary_key: true + primary_key: true, + null: false ) timestamps() @@ -91,19 +85,497 @@ defmodule Explorer.Chain.Token.Instance do def page_token_instance(query, _), do: query def owner_query(%Instance{token_contract_address_hash: token_contract_address_hash, token_id: token_id}) do - from( - tt in TokenTransfer, - join: to_address in assoc(tt, :to_address), - where: - tt.token_contract_address_hash == ^token_contract_address_hash and - fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^token_id), - order_by: [desc: tt.block_number], - limit: 1, - select: to_address + CurrentTokenBalance + |> where( + [ctb], + ctb.token_contract_address_hash == ^token_contract_address_hash and ctb.token_id == ^token_id and ctb.value > 0 ) + |> limit(1) + |> select([ctb], ctb.address_hash) end - @spec token_instance_query(non_neg_integer(), Hash.Address.t()) :: Ecto.Query.t() + @spec token_instance_query(Decimal.t() | non_neg_integer(), Hash.Address.t()) :: Ecto.Query.t() def token_instance_query(token_id, token_contract_address), do: from(i in Instance, where: i.token_contract_address_hash == ^token_contract_address and i.token_id == ^token_id) + + @spec nft_list(binary() | Hash.Address.t(), keyword()) :: [Instance.t()] + def nft_list(address_hash, options \\ []) + + def nft_list(address_hash, options) when is_list(options) do + nft_list(address_hash, Keyword.get(options, :token_type, []), options) + end + + defp nft_list(address_hash, ["ERC-721"], options) do + erc_721_token_instances_by_owner_address_hash(address_hash, options) + end + + defp nft_list(address_hash, ["ERC-1155"], options) do + erc_1155_token_instances_by_address_hash(address_hash, options) + end + + defp nft_list(address_hash, ["ERC-404"], options) do + erc_404_token_instances_by_address_hash(address_hash, options) + end + + defp nft_list(address_hash, _, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + case paging_options do + %PagingOptions{key: {_contract_address_hash, _token_id, "ERC-1155"}} -> + erc_1155_token_instances_by_address_hash(address_hash, options) + + %PagingOptions{key: {_contract_address_hash, _token_id, "ERC-404"}} -> + erc_404_token_instances_by_address_hash(address_hash, options) + + _ -> + erc_721 = erc_721_token_instances_by_owner_address_hash(address_hash, options) + + if length(erc_721) == paging_options.page_size do + erc_721 + else + erc_1155 = erc_1155_token_instances_by_address_hash(address_hash, options) + erc_404 = erc_404_token_instances_by_address_hash(address_hash, options) + + (erc_721 ++ erc_1155 ++ erc_404) |> Enum.take(paging_options.page_size) + end + end + end + + @doc """ + In this function used fact that only ERC-721 instances has NOT NULL owner_address_hash. + """ + @spec erc_721_token_instances_by_owner_address_hash(binary() | Hash.Address.t(), keyword) :: [Instance.t()] + def erc_721_token_instances_by_owner_address_hash(address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + __MODULE__ + |> where([ti], ti.owner_address_hash == ^address_hash) + |> order_by([ti], asc: ti.token_contract_address_hash, desc: ti.token_id) + |> limit(^paging_options.page_size) + |> page_erc_721_token_instances(paging_options) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + + defp page_erc_721_token_instances(query, %PagingOptions{key: {contract_address_hash, token_id, "ERC-721"}}) do + page_token_instance(query, contract_address_hash, token_id) + end + + defp page_erc_721_token_instances(query, _), do: query + + @spec erc_1155_token_instances_by_address_hash(binary() | Hash.Address.t(), keyword) :: [Instance.t()] + def erc_1155_token_instances_by_address_hash(address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + __MODULE__ + |> join(:inner, [ti], ctb in CurrentTokenBalance, + as: :ctb, + on: + ctb.token_contract_address_hash == ti.token_contract_address_hash and ctb.token_id == ti.token_id and + ctb.address_hash == ^address_hash + ) + |> where([ctb: ctb], ctb.value > 0 and ctb.token_type == "ERC-1155") + |> order_by([ti], asc: ti.token_contract_address_hash, desc: ti.token_id) + |> limit(^paging_options.page_size) + |> page_erc_1155_token_instances(paging_options) + |> select_merge([ctb: ctb], %{current_token_balance: ctb}) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + + defp page_erc_1155_token_instances(query, %PagingOptions{key: {contract_address_hash, token_id, "ERC-1155"}}) do + page_token_instance(query, contract_address_hash, token_id) + end + + defp page_erc_1155_token_instances(query, _), do: query + + @spec erc_404_token_instances_by_address_hash(binary() | Hash.Address.t(), keyword) :: [Instance.t()] + def erc_404_token_instances_by_address_hash(address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + __MODULE__ + |> join(:inner, [ti], ctb in CurrentTokenBalance, + as: :ctb, + on: + ctb.token_contract_address_hash == ti.token_contract_address_hash and ctb.token_id == ti.token_id and + ctb.address_hash == ^address_hash + ) + |> where([ctb: ctb], ctb.value > 0 and ctb.token_type == "ERC-404") + |> order_by([ti], asc: ti.token_contract_address_hash, desc: ti.token_id) + |> limit(^paging_options.page_size) + |> page_erc_404_token_instances(paging_options) + |> select_merge([ctb: ctb], %{current_token_balance: ctb}) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + + defp page_erc_404_token_instances(query, %PagingOptions{key: {contract_address_hash, token_id, "ERC-404"}}) do + page_token_instance(query, contract_address_hash, token_id) + end + + defp page_erc_404_token_instances(query, _), do: query + + defp page_token_instance(query, contract_address_hash, token_id) do + query + |> where( + [ti], + ti.token_contract_address_hash > ^contract_address_hash or + (ti.token_contract_address_hash == ^contract_address_hash and ti.token_id < ^token_id) + ) + end + + @doc """ + Function to be used in BlockScoutWeb.Chain.next_page_params/4 + """ + @spec nft_list_next_page_params(Explorer.Chain.Token.Instance.t()) :: %{binary() => any} + def nft_list_next_page_params(%__MODULE__{ + current_token_balance: %CurrentTokenBalance{}, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id, + token: token + }) do + %{"token_contract_address_hash" => token_contract_address_hash, "token_id" => token_id, "token_type" => token.type} + end + + def nft_list_next_page_params(%__MODULE__{ + token_contract_address_hash: token_contract_address_hash, + token_id: token_id + }) do + %{"token_contract_address_hash" => token_contract_address_hash, "token_id" => token_id, "token_type" => "ERC-721"} + end + + @preloaded_nfts_limit 9 + + @spec nft_collections(binary() | Hash.Address.t(), keyword) :: list + def nft_collections(address_hash, options \\ []) + + def nft_collections(address_hash, options) when is_list(options) do + nft_collections(address_hash, Keyword.get(options, :token_type, []), options) + end + + defp nft_collections(address_hash, ["ERC-721"], options) do + erc_721_collections_by_address_hash(address_hash, options) + end + + defp nft_collections(address_hash, ["ERC-1155"], options) do + erc_1155_collections_by_address_hash(address_hash, options) + end + + defp nft_collections(address_hash, ["ERC-404"], options) do + erc_404_collections_by_address_hash(address_hash, options) + end + + defp nft_collections(address_hash, _, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + case paging_options do + %PagingOptions{key: {_contract_address_hash, "ERC-1155"}} -> + erc_1155_collections_by_address_hash(address_hash, options) + + _ -> + erc_721 = erc_721_collections_by_address_hash(address_hash, options) + + if length(erc_721) == paging_options.page_size do + erc_721 + else + erc_1155 = erc_1155_collections_by_address_hash(address_hash, options) + erc_404 = erc_404_collections_by_address_hash(address_hash, options) + + (erc_721 ++ erc_1155 ++ erc_404) |> Enum.take(paging_options.page_size) + end + end + end + + @spec erc_721_collections_by_address_hash(binary() | Hash.Address.t(), keyword) :: [CurrentTokenBalance.t()] + def erc_721_collections_by_address_hash(address_hash, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + CurrentTokenBalance + |> where([ctb], ctb.address_hash == ^address_hash and ctb.value > 0 and ctb.token_type == "ERC-721") + |> order_by([ctb], asc: ctb.token_contract_address_hash) + |> page_erc_721_nft_collections(paging_options) + |> limit(^paging_options.page_size) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + |> Enum.map(&erc_721_preload_nft(&1, options)) + end + + defp page_erc_721_nft_collections(query, %PagingOptions{key: {contract_address_hash, "ERC-721"}}) do + page_nft_collections(query, contract_address_hash) + end + + defp page_erc_721_nft_collections(query, _), do: query + + @spec erc_1155_collections_by_address_hash(binary() | Hash.Address.t(), keyword) :: [ + %{ + token_contract_address_hash: Hash.Address.t(), + distinct_token_instances_count: integer(), + token_ids: [integer()] + } + ] + def erc_1155_collections_by_address_hash(address_hash, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + CurrentTokenBalance + |> where([ctb], ctb.address_hash == ^address_hash and ctb.value > 0 and ctb.token_type == "ERC-1155") + |> group_by([ctb], ctb.token_contract_address_hash) + |> order_by([ctb], asc: ctb.token_contract_address_hash) + |> select([ctb], %{ + token_contract_address_hash: ctb.token_contract_address_hash, + distinct_token_instances_count: fragment("COUNT(*)"), + token_ids: fragment("array_agg(?)", ctb.token_id) + }) + |> page_erc_1155_nft_collections(paging_options) + |> limit(^paging_options.page_size) + |> Chain.select_repo(options).all() + |> Enum.map(&erc_1155_preload_nft(&1, address_hash, options)) + |> Helper.custom_preload(options, Token, :token_contract_address_hash, :contract_address_hash, :token) + end + + defp page_erc_1155_nft_collections(query, %PagingOptions{key: {contract_address_hash, "ERC-1155"}}) do + page_nft_collections(query, contract_address_hash) + end + + defp page_erc_1155_nft_collections(query, _), do: query + + @spec erc_404_collections_by_address_hash(binary() | Hash.Address.t(), keyword) :: [ + %{ + token_contract_address_hash: Hash.Address.t(), + distinct_token_instances_count: integer(), + token_ids: [integer()] + } + ] + def erc_404_collections_by_address_hash(address_hash, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + CurrentTokenBalance + |> where([ctb], ctb.address_hash == ^address_hash and ctb.value > 0 and ctb.token_type == "ERC-404") + |> group_by([ctb], ctb.token_contract_address_hash) + |> order_by([ctb], asc: ctb.token_contract_address_hash) + |> select([ctb], %{ + token_contract_address_hash: ctb.token_contract_address_hash, + distinct_token_instances_count: fragment("COUNT(*)"), + token_ids: fragment("array_agg(?)", ctb.token_id) + }) + |> page_erc_404_nft_collections(paging_options) + |> limit(^paging_options.page_size) + |> Chain.select_repo(options).all() + |> Enum.map(&erc_1155_preload_nft(&1, address_hash, options)) + |> Helper.custom_preload(options, Token, :token_contract_address_hash, :contract_address_hash, :token) + end + + defp page_erc_404_nft_collections(query, %PagingOptions{key: {contract_address_hash, "ERC-404"}}) do + page_nft_collections(query, contract_address_hash) + end + + defp page_erc_404_nft_collections(query, _), do: query + + defp page_nft_collections(query, token_contract_address_hash) do + query + |> where([ctb], ctb.token_contract_address_hash > ^token_contract_address_hash) + end + + defp erc_721_preload_nft( + %CurrentTokenBalance{token_contract_address_hash: token_contract_address_hash, address_hash: address_hash} = + ctb, + options + ) do + instances = + Instance + |> where( + [ti], + ti.token_contract_address_hash == ^token_contract_address_hash and ti.owner_address_hash == ^address_hash + ) + |> order_by([ti], desc: ti.token_id) + |> limit(^@preloaded_nfts_limit) + |> Chain.select_repo(options).all() + + %CurrentTokenBalance{ctb | preloaded_token_instances: instances} + end + + defp erc_1155_preload_nft( + %{token_contract_address_hash: token_contract_address_hash, token_ids: token_ids} = collection, + address_hash, + options + ) do + token_ids = token_ids |> Enum.sort(:desc) |> Enum.take(@preloaded_nfts_limit) + + instances = + Instance + |> where([ti], ti.token_contract_address_hash == ^token_contract_address_hash and ti.token_id in ^token_ids) + |> join(:inner, [ti], ctb in CurrentTokenBalance, + as: :ctb, + on: + ctb.token_contract_address_hash == ti.token_contract_address_hash and ti.token_id == ctb.token_id and + ctb.address_hash == ^address_hash + ) + |> limit(^@preloaded_nfts_limit) + |> select_merge([ctb: ctb], %{current_token_balance: ctb}) + |> Chain.select_repo(options).all() + |> Enum.sort_by(& &1.token_id, :desc) + + Map.put(collection, :preloaded_token_instances, instances) + end + + @doc """ + Function to be used in BlockScoutWeb.Chain.next_page_params/4 + """ + @spec nft_collections_next_page_params(%{:token_contract_address_hash => any, optional(any) => any}) :: %{ + binary() => any + } + def nft_collections_next_page_params(%{ + token_contract_address_hash: token_contract_address_hash, + token: %Token{type: token_type} + }) do + %{"token_contract_address_hash" => token_contract_address_hash, "token_type" => token_type} + end + + def nft_collections_next_page_params(%{ + token_contract_address_hash: token_contract_address_hash, + token_type: token_type + }) do + %{"token_contract_address_hash" => token_contract_address_hash, "token_type" => token_type} + end + + @spec token_instances_by_holder_address_hash(Token.t(), binary() | Hash.Address.t(), keyword) :: [Instance.t()] + def token_instances_by_holder_address_hash(token, holder_address_hash, options \\ []) + + def token_instances_by_holder_address_hash(%Token{type: "ERC-721"} = token, holder_address_hash, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + token.contract_address_hash + |> address_to_unique_token_instances() + |> where([ti], ti.owner_address_hash == ^holder_address_hash) + |> limit(^paging_options.page_size) + |> page_token_instance(paging_options) + |> Chain.select_repo(options).all() + |> Enum.map(&put_is_unique(&1, token, options)) + end + + def token_instances_by_holder_address_hash(%Token{} = token, holder_address_hash, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + __MODULE__ + |> where([ti], ti.token_contract_address_hash == ^token.contract_address_hash) + |> join(:inner, [ti], ctb in CurrentTokenBalance, + as: :ctb, + on: + ctb.token_contract_address_hash == ti.token_contract_address_hash and ctb.token_id == ti.token_id and + ctb.address_hash == ^holder_address_hash + ) + |> where([ctb: ctb], ctb.value > 0) + |> order_by([ti], desc: ti.token_id) + |> limit(^paging_options.page_size) + |> page_token_instance(paging_options) + |> select_merge([ctb: ctb], %{current_token_balance: ctb}) + |> Chain.select_repo(options).all() + |> Enum.map(&put_is_unique(&1, token, options)) + end + + @doc """ + Finds token instances (pairs of contract_address_hash and token_id) which was met in token transfers but has no corresponding entry in token_instances table + """ + @spec not_inserted_token_instances_query(integer()) :: Ecto.Query.t() + def not_inserted_token_instances_query(limit) do + token_transfers_query = + TokenTransfer + |> where([token_transfer], not is_nil(token_transfer.token_ids) and token_transfer.token_ids != ^[]) + |> select([token_transfer], %{ + token_contract_address_hash: token_transfer.token_contract_address_hash, + token_id: fragment("unnest(?)", token_transfer.token_ids) + }) + + token_transfers_query + |> subquery() + |> join(:left, [token_transfer], token_instance in __MODULE__, + on: + token_instance.token_contract_address_hash == token_transfer.token_contract_address_hash and + token_instance.token_id == token_transfer.token_id + ) + |> where([token_transfer, token_instance], is_nil(token_instance.token_id)) + |> select([token_transfer, token_instance], %{ + contract_address_hash: token_transfer.token_contract_address_hash, + token_id: token_transfer.token_id + }) + |> limit(^limit) + end + + @doc """ + Finds token instances of a particular token (pairs of contract_address_hash and token_id) which was met in token_transfers table but has no corresponding entry in token_instances table. + """ + @spec not_inserted_token_instances_query_by_token(integer(), Hash.Address.t()) :: Ecto.Query.t() + def not_inserted_token_instances_query_by_token(limit, token_contract_address_hash) do + token_transfers_query = + TokenTransfer + |> where([token_transfer], token_transfer.token_contract_address_hash == ^token_contract_address_hash) + |> select([token_transfer], %{ + token_contract_address_hash: token_transfer.token_contract_address_hash, + token_id: fragment("unnest(?)", token_transfer.token_ids) + }) + + token_transfers_query + |> subquery() + |> join(:left, [token_transfer], token_instance in __MODULE__, + on: + token_instance.token_contract_address_hash == token_transfer.token_contract_address_hash and + token_instance.token_id == token_transfer.token_id + ) + |> where([token_transfer, token_instance], is_nil(token_instance.token_id)) + |> select([token_transfer, token_instance], %{ + contract_address_hash: token_transfer.token_contract_address_hash, + token_id: token_transfer.token_id + }) + |> limit(^limit) + end + + @doc """ + Finds ERC-1155 token instances (pairs of contract_address_hash and token_id) which was met in current_token_balances table but has no corresponding entry in token_instances table. + """ + @spec not_inserted_erc_1155_token_instances(integer()) :: Ecto.Query.t() + def not_inserted_erc_1155_token_instances(limit) do + CurrentTokenBalance + |> join(:left, [actb], ti in __MODULE__, + on: actb.token_contract_address_hash == ti.token_contract_address_hash and actb.token_id == ti.token_id + ) + |> where([actb, ti], not is_nil(actb.token_id) and is_nil(ti.token_id)) + |> select([actb], %{ + contract_address_hash: actb.token_contract_address_hash, + token_id: actb.token_id + }) + |> limit(^limit) + end + + @doc """ + Puts is_unique field in token instance. Returns updated token instance + is_unique is true for ERC-721 always and for ERC-1155 only if token_id is unique + """ + @spec put_is_unique(Instance.t(), Token.t(), Keyword.t()) :: Instance.t() + def put_is_unique(instance, token, options) do + %__MODULE__{instance | is_unique: unique?(instance, token, options)} + end + + defp unique?( + %Instance{current_token_balance: %CurrentTokenBalance{value: %Decimal{} = value}} = instance, + token, + options + ) do + if Decimal.compare(value, 1) == :gt do + false + else + unique?(%Instance{instance | current_token_balance: nil}, token, options) + end + end + + defp unique?(%Instance{current_token_balance: %CurrentTokenBalance{value: value}}, _token, _options) + when value > 1, + do: false + + defp unique?(instance, token, options), + do: + not (token.type == "ERC-1155") or + Chain.token_id_1155_is_unique?(token.contract_address_hash, instance.token_id, options) end diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 1c5a15123f51..4f1e4495decc 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -25,15 +25,27 @@ defmodule Explorer.Chain.TokenTransfer do use Explorer.Schema import Ecto.Changeset - import Ecto.Query, only: [from: 2, limit: 2, where: 3, join: 5, order_by: 3, preload: 3] alias Explorer.Chain - alias Explorer.Chain.{Address, Block, Hash, TokenTransfer, Transaction} + alias Explorer.Chain.{Address, Block, DenormalizationHelper, Hash, Log, TokenTransfer, Transaction} alias Explorer.Chain.Token.Instance alias Explorer.{PagingOptions, Repo} @default_paging_options %PagingOptions{page_size: 50} + @typep paging_options :: {:paging_options, PagingOptions.t()} + @typep api? :: {:api?, true | false} + + @constant "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @weth_deposit_signature "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" + @weth_withdrawal_signature "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @erc1155_single_transfer_signature "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" + @erc1155_batch_transfer_signature "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb" + @erc404_erc20_transfer_event "0xe59fdd36d0d223c0c7d996db7ad796880f45e1936cb0bb7ac102e7082e031487" + @erc404_erc721_transfer_event "0xe5f815dc84b8cecdfd4beedfc3f91ab5be7af100eca4e8fb11552b867995394f" + + @transfer_function_signature "0xa9059cbb" + @typedoc """ * `:amount` - The token transferred amount * `:block_hash` - hash of the block @@ -44,76 +56,56 @@ defmodule Explorer.Chain.TokenTransfer do * `:to_address_hash` - Address hash foreign key * `:token_contract_address` - The `t:Explorer.Chain.Address.t/0` of the token's contract. * `:token_contract_address_hash` - Address hash foreign key - * `:token_id` - ID of the token (applicable to ERC-721 tokens) * `:transaction` - The `t:Explorer.Chain.Transaction.t/0` ledger * `:transaction_hash` - Transaction foreign key * `:log_index` - Index of the corresponding `t:Explorer.Chain.Log.t/0` in the block. * `:amounts` - Tokens transferred amounts in case of batched transfer in ERC-1155 * `:token_ids` - IDs of the tokens (applicable to ERC-1155 tokens) + * `:block_consensus` - Consensus of the block that the transfer took place """ - @type t :: %TokenTransfer{ - amount: Decimal.t() | nil, - block_number: non_neg_integer() | nil, - block_hash: Hash.Full.t(), - from_address: %Ecto.Association.NotLoaded{} | Address.t(), - from_address_hash: Hash.Address.t(), - to_address: %Ecto.Association.NotLoaded{} | Address.t(), - to_address_hash: Hash.Address.t(), - token_contract_address: %Ecto.Association.NotLoaded{} | Address.t(), - token_contract_address_hash: Hash.Address.t(), - token_id: non_neg_integer() | nil, - transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), - transaction_hash: Hash.Full.t(), - log_index: non_neg_integer(), - amounts: [Decimal.t()] | nil, - token_ids: [non_neg_integer()] | nil, - index_in_batch: non_neg_integer() | nil - } - - @typep paging_options :: {:paging_options, PagingOptions.t()} - @typep api? :: {:api?, true | false} - - @constant "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" - @weth_deposit_signature "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" - @weth_withdrawal_signature "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" - @erc1155_single_transfer_signature "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" - @erc1155_batch_transfer_signature "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb" - - @transfer_function_signature "0xa9059cbb" - @primary_key false - schema "token_transfers" do + typed_schema "token_transfers" do field(:amount, :decimal) - field(:block_number, :integer) - field(:log_index, :integer, primary_key: true) - field(:token_id, :decimal) + field(:block_number, :integer) :: Block.block_number() + field(:log_index, :integer, primary_key: true, null: false) field(:amounts, {:array, :decimal}) field(:token_ids, {:array, :decimal}) field(:index_in_batch, :integer, virtual: true) + field(:token_type, :string) + field(:block_consensus, :boolean) + + belongs_to(:from_address, Address, + foreign_key: :from_address_hash, + references: :hash, + type: Hash.Address, + null: false + ) - belongs_to(:from_address, Address, foreign_key: :from_address_hash, references: :hash, type: Hash.Address) - belongs_to(:to_address, Address, foreign_key: :to_address_hash, references: :hash, type: Hash.Address) + belongs_to(:to_address, Address, foreign_key: :to_address_hash, references: :hash, type: Hash.Address, null: false) belongs_to( :token_contract_address, Address, foreign_key: :token_contract_address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false ) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, primary_key: true, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) belongs_to(:block, Block, foreign_key: :block_hash, primary_key: true, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) has_many( @@ -128,8 +120,8 @@ defmodule Explorer.Chain.TokenTransfer do timestamps() end - @required_attrs ~w(block_number log_index from_address_hash to_address_hash token_contract_address_hash transaction_hash block_hash)a - @optional_attrs ~w(amount token_id amounts token_ids)a + @required_attrs ~w(block_number log_index from_address_hash to_address_hash token_contract_address_hash transaction_hash block_hash token_type)a + @optional_attrs ~w(amount amounts token_ids block_consensus)a @doc false def changeset(%TokenTransfer{} = struct, params \\ %{}) do @@ -153,6 +145,10 @@ defmodule Explorer.Chain.TokenTransfer do def erc1155_batch_transfer_signature, do: @erc1155_batch_transfer_signature + def erc404_erc20_transfer_event, do: @erc404_erc20_transfer_event + + def erc404_erc721_transfer_event, do: @erc404_erc721_transfer_event + @doc """ ERC 20's transfer(address,uint256) function signature """ @@ -161,16 +157,12 @@ defmodule Explorer.Chain.TokenTransfer do @spec fetch_token_transfers_from_token_hash(Hash.t(), [paging_options | api?]) :: [] def fetch_token_transfers_from_token_hash(token_address_hash, options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) + preloads = DenormalizationHelper.extend_transaction_preload([:transaction, :token, :from_address, :to_address]) - query = - from( - tt in TokenTransfer, - where: tt.token_contract_address_hash == ^token_address_hash and not is_nil(tt.block_number), - preload: [{:transaction, :block}, :token, :from_address, :to_address], - order_by: [desc: tt.block_number, desc: tt.log_index] - ) - - query + only_consensus_transfers_query() + |> where([tt], tt.token_contract_address_hash == ^token_address_hash and not is_nil(tt.block_number)) + |> preload(^preloads) + |> order_by([tt], desc: tt.block_number, desc: tt.log_index) |> page_token_transfer(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() @@ -179,18 +171,14 @@ defmodule Explorer.Chain.TokenTransfer do @spec fetch_token_transfers_from_token_hash_and_token_id(Hash.t(), non_neg_integer(), [paging_options | api?]) :: [] def fetch_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) + preloads = DenormalizationHelper.extend_transaction_preload([:transaction, :token, :from_address, :to_address]) - query = - from( - tt in TokenTransfer, - where: tt.token_contract_address_hash == ^token_address_hash, - where: fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id)), - where: not is_nil(tt.block_number), - preload: [{:transaction, :block}, :token, :from_address, :to_address], - order_by: [desc: tt.block_number, desc: tt.log_index] - ) - - query + only_consensus_transfers_query() + |> where([tt], tt.token_contract_address_hash == ^token_address_hash) + |> where([tt], fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^Decimal.new(token_id))) + |> where([tt], not is_nil(tt.block_number)) + |> preload(^preloads) + |> order_by([tt], desc: tt.block_number, desc: tt.log_index) |> page_token_transfer(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() @@ -323,19 +311,46 @@ defmodule Explorer.Chain.TokenTransfer do end def token_transfers_by_address_hash_and_token_address_hash(address_hash, token_address_hash) do - TokenTransfer + only_consensus_transfers_query() |> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash) |> where([tt], tt.token_contract_address_hash == ^token_address_hash) |> order_by([tt], desc: tt.block_number, desc: tt.log_index) end - def token_transfers_by_address_hash(direction, address_hash, token_types) do - TokenTransfer - |> filter_by_direction(direction, address_hash) - |> order_by([tt], desc: tt.block_number, desc: tt.log_index) - |> join(:inner, [tt], token in assoc(tt, :token), as: :token) - |> preload([token: token], [{:token, token}]) - |> filter_by_type(token_types) + def token_transfers_by_address_hash(direction, address_hash, token_types, paging_options) do + if direction == :to || direction == :from do + only_consensus_transfers_query() + |> filter_by_direction(direction, address_hash) + |> order_by([tt], desc: tt.block_number, desc: tt.log_index) + |> join(:inner, [tt], token in assoc(tt, :token), as: :token) + |> preload([token: token], [{:token, token}]) + |> filter_by_type(token_types) + |> handle_paging_options(paging_options) + else + to_address_hash_query = + only_consensus_transfers_query() + |> join(:inner, [tt], token in assoc(tt, :token), as: :token) + |> filter_by_direction(:to, address_hash) + |> filter_by_type(token_types) + |> order_by([tt], desc: tt.block_number, desc: tt.log_index) + |> handle_paging_options(paging_options) + |> Chain.wrapped_union_subquery() + + from_address_hash_query = + only_consensus_transfers_query() + |> join(:inner, [tt], token in assoc(tt, :token), as: :token) + |> filter_by_direction(:from, address_hash) + |> filter_by_type(token_types) + |> order_by([tt], desc: tt.block_number, desc: tt.log_index) + |> handle_paging_options(paging_options) + |> Chain.wrapped_union_subquery() + + to_address_hash_query + |> union(^from_address_hash_query) + |> Chain.wrapped_union_subquery() + |> order_by([tt], desc: tt.block_number, desc: tt.log_index) + |> limit(^paging_options.page_size) + end end def filter_by_direction(query, :to, address_hash) do @@ -348,24 +363,122 @@ defmodule Explorer.Chain.TokenTransfer do |> where([tt], tt.from_address_hash == ^address_hash) end - def filter_by_direction(query, _, address_hash) do - query - |> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash) - end - def filter_by_type(query, []), do: query def filter_by_type(query, token_types) when is_list(token_types) do - where(query, [token: token], token.type in ^token_types) + if DenormalizationHelper.tt_denormalization_finished?() do + where(query, [tt], tt.token_type in ^token_types) + else + where(query, [token: token], token.type in ^token_types) + end end def filter_by_type(query, _), do: query + @doc """ + Returns ecto query to fetch consensus token transfers + """ + @spec only_consensus_transfers_query() :: Ecto.Query.t() def only_consensus_transfers_query do - from(token_transfer in __MODULE__, - inner_join: block in Block, - on: token_transfer.block_hash == block.hash, - where: block.consensus == true + if DenormalizationHelper.tt_denormalization_finished?() do + from(token_transfer in __MODULE__, where: token_transfer.block_consensus == true) + else + from(token_transfer in __MODULE__, + inner_join: block in assoc(token_transfer, :block), + as: :block, + where: block.consensus == true + ) + end + end + + @doc """ + Returns a list of block numbers token transfer `t:Log.t/0`s that don't have an + associated `t:TokenTransfer.t/0` record. + """ + @spec uncataloged_token_transfer_block_numbers :: {:ok, [non_neg_integer()]} + def uncataloged_token_transfer_block_numbers do + query = + from(l in Log, + as: :log, + where: + l.first_topic == ^@constant or + l.first_topic == ^@erc1155_single_transfer_signature or + l.first_topic == ^@erc1155_batch_transfer_signature, + where: + not exists( + from(tf in TokenTransfer, + where: tf.transaction_hash == parent_as(:log).transaction_hash, + where: tf.log_index == parent_as(:log).index + ) + ), + select: l.block_number, + distinct: l.block_number + ) + + Repo.stream_reduce(query, [], &[&1 | &2]) + end + + @doc """ + Returns ecto query to fetch consensus token transfers with ERC-721 token type + """ + @spec erc_721_token_transfers_query() :: Ecto.Query.t() + def erc_721_token_transfers_query do + only_consensus_transfers_query() + |> join(:inner, [tt], token in assoc(tt, :token), as: :token) + |> where([tt, token: token], token.type == "ERC-721") + |> preload([tt, token: token], [{:token, token}]) + end + + @doc """ + To be used in migrators + """ + @spec encode_token_transfer_ids([{Hash.t(), Hash.t(), non_neg_integer()}]) :: binary() + def encode_token_transfer_ids(ids) do + encoded_values = + ids + |> Enum.reduce("", fn {t_hash, b_hash, log_index}, acc -> + acc <> "('#{hash_to_query_string(t_hash)}', '#{hash_to_query_string(b_hash)}', #{log_index})," + end) + |> String.trim_trailing(",") + + "(#{encoded_values})" + end + + defp hash_to_query_string(hash) do + s_hash = + hash + |> to_string() + |> String.trim_leading("0") + + "\\#{s_hash}" + end + + @doc """ + Fetches token transfers from logs. + """ + @spec logs_to_token_transfers([Log.t()], Keyword.t()) :: [TokenTransfer.t()] + def logs_to_token_transfers(logs, options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + logs + |> logs_to_token_transfers_query() + |> limit(^Enum.count(logs)) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + + defp logs_to_token_transfers_query(query \\ __MODULE__, logs) + + defp logs_to_token_transfers_query(query, [log | tail]) do + query + |> or_where( + [tt], + tt.transaction_hash == ^log.transaction_hash and tt.block_hash == ^log.block_hash and tt.log_index == ^log.index ) + |> logs_to_token_transfers_query(tail) + end + + defp logs_to_token_transfers_query(query, []) do + query end end diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 60e9cb3d2c4f..8f168a4cd699 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -1,46 +1,246 @@ +defmodule Explorer.Chain.Transaction.Schema do + @moduledoc false + + alias Explorer.Chain.{ + Address, + Beacon.BlobTransaction, + Block, + Data, + Hash, + InternalTransaction, + Log, + TokenTransfer, + TransactionAction, + Wei + } + + alias Explorer.Chain.PolygonZkevm.BatchTransaction, as: ZkevmBatchTransaction + alias Explorer.Chain.Transaction.{Fork, Status} + alias Explorer.Chain.ZkSync.BatchTransaction, as: ZkSyncBatchTransaction + + @chain_type_fields (case Application.compile_env(:explorer, :chain_type) do + "ethereum" -> + # elem(quote do ... end, 2) doesn't work with a single has_one instruction + quote do + [ + has_one(:beacon_blob_transaction, BlobTransaction, foreign_key: :hash, references: :hash) + ] + end + + "optimism" -> + elem( + quote do + field(:l1_fee, Wei) + field(:l1_fee_scalar, :decimal) + field(:l1_gas_price, Wei) + field(:l1_gas_used, :decimal) + field(:l1_tx_origin, Hash.Full) + field(:l1_block_number, :integer) + end, + 2 + ) + + "suave" -> + elem( + quote do + belongs_to( + :execution_node, + Address, + foreign_key: :execution_node_hash, + references: :hash, + type: Hash.Address + ) + + field(:wrapped_type, :integer) + field(:wrapped_nonce, :integer) + field(:wrapped_gas, :decimal) + field(:wrapped_gas_price, Wei) + field(:wrapped_max_priority_fee_per_gas, Wei) + field(:wrapped_max_fee_per_gas, Wei) + field(:wrapped_value, Wei) + field(:wrapped_input, Data) + field(:wrapped_v, :decimal) + field(:wrapped_r, :decimal) + field(:wrapped_s, :decimal) + field(:wrapped_hash, Hash.Full) + + belongs_to( + :wrapped_to_address, + Address, + foreign_key: :wrapped_to_address_hash, + references: :hash, + type: Hash.Address + ) + end, + 2 + ) + + "polygon_zkevm" -> + elem( + quote do + has_one(:zkevm_batch_transaction, ZkevmBatchTransaction, + foreign_key: :hash, + references: :hash + ) + + has_one(:zkevm_batch, through: [:zkevm_batch_transaction, :batch], references: :hash) + + has_one(:zkevm_sequence_transaction, + through: [:zkevm_batch, :sequence_transaction], + references: :hash + ) + + has_one(:zkevm_verify_transaction, + through: [:zkevm_batch, :verify_transaction], + references: :hash + ) + end, + 2 + ) + + "zksync" -> + elem( + quote do + has_one(:zksync_batch_transaction, ZkSyncBatchTransaction, + foreign_key: :hash, + references: :hash + ) + + has_one(:zksync_batch, through: [:zksync_batch_transaction, :batch]) + has_one(:zksync_commit_transaction, through: [:zksync_batch, :commit_transaction]) + has_one(:zksync_prove_transaction, through: [:zksync_batch, :prove_transaction]) + has_one(:zksync_execute_transaction, through: [:zksync_batch, :execute_transaction]) + end, + 2 + ) + + _ -> + [] + end) + + defmacro generate do + quote do + @primary_key false + typed_schema "transactions" do + field(:hash, Hash.Full, primary_key: true) + field(:block_number, :integer) + field(:block_consensus, :boolean) + field(:block_timestamp, :utc_datetime_usec) + field(:cumulative_gas_used, :decimal) + field(:earliest_processing_start, :utc_datetime_usec) + field(:error, :string) + field(:gas, :decimal) + field(:gas_price, Wei) + field(:gas_used, :decimal) + field(:index, :integer) + field(:created_contract_code_indexed_at, :utc_datetime_usec) + field(:input, Data) + field(:nonce, :integer) :: non_neg_integer() | nil + field(:r, :decimal) + field(:s, :decimal) + field(:status, Status) + field(:v, :decimal) + field(:value, Wei) + field(:revert_reason, :string) + field(:max_priority_fee_per_gas, Wei) + field(:max_fee_per_gas, Wei) + field(:type, :integer) + field(:has_error_in_internal_txs, :boolean) + field(:has_token_transfers, :boolean, virtual: true) + + # stability virtual fields + field(:transaction_fee_log, :any, virtual: true) + field(:transaction_fee_token, :any, virtual: true) + + # A transient field for deriving old block hash during transaction upserts. + # Used to force refetch of a block in case a transaction is re-collated + # in a different block. See: https://github.com/blockscout/blockscout/issues/1911 + field(:old_block_hash, Hash.Full) + + timestamps() + + belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full) + has_many(:forks, Fork, foreign_key: :hash, references: :hash) + + belongs_to( + :from_address, + Address, + foreign_key: :from_address_hash, + references: :hash, + type: Hash.Address + ) + + has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash, references: :hash) + has_many(:logs, Log, foreign_key: :transaction_hash, references: :hash) + + has_many(:token_transfers, TokenTransfer, foreign_key: :transaction_hash, references: :hash) + + has_many(:transaction_actions, TransactionAction, + foreign_key: :hash, + preload_order: [asc: :log_index], + references: :hash + ) + + belongs_to( + :to_address, + Address, + foreign_key: :to_address_hash, + references: :hash, + type: Hash.Address + ) + + has_many(:uncles, through: [:forks, :uncle], references: :hash) + + belongs_to( + :created_contract_address, + Address, + foreign_key: :created_contract_address_hash, + references: :hash, + type: Hash.Address + ) + + unquote_splicing(@chain_type_fields) + end + end + end +end + defmodule Explorer.Chain.Transaction do @moduledoc "Models a Web3 transaction." use Explorer.Schema require Logger - - import Ecto.Query, only: [from: 2, preload: 3, subquery: 1, where: 3] + require Explorer.Chain.Transaction.Schema alias ABI.FunctionSelector - alias Ecto.Association.NotLoaded alias Ecto.Changeset - - alias Explorer.Chain + alias Explorer.{Chain, PagingOptions, Repo, SortingHelper} alias Explorer.Chain.{ - Address, - Block, + Block.Reward, ContractMethod, Data, - Gas, + DenormalizationHelper, Hash, - InternalTransaction, - Log, SmartContract, - Token, + SmartContract.Proxy, TokenTransfer, Transaction, - TransactionAction, Wei } - alias Explorer.Chain.Transaction.{Fork, Status} - alias Explorer.Chain.Zkevm.BatchTransaction alias Explorer.SmartContract.SigProviderInterface - @optional_attrs ~w(max_priority_fee_per_gas max_fee_per_gas block_hash block_number created_contract_address_hash cumulative_gas_used earliest_processing_start - error gas_price gas_used index created_contract_code_indexed_at status to_address_hash revert_reason type has_error_in_internal_txs)a + @optional_attrs ~w(max_priority_fee_per_gas max_fee_per_gas block_hash block_number block_consensus block_timestamp created_contract_address_hash cumulative_gas_used earliest_processing_start + error gas_price gas_used index created_contract_code_indexed_at status + to_address_hash revert_reason type has_error_in_internal_txs r s v)a + @optimism_optional_attrs ~w(l1_fee l1_fee_scalar l1_gas_price l1_gas_used l1_tx_origin l1_block_number)a @suave_optional_attrs ~w(execution_node_hash wrapped_type wrapped_nonce wrapped_to_address_hash wrapped_gas wrapped_gas_price wrapped_max_priority_fee_per_gas wrapped_max_fee_per_gas wrapped_value wrapped_input wrapped_v wrapped_r wrapped_s wrapped_hash)a - @required_attrs ~w(from_address_hash gas hash input nonce r s v value)a + @required_attrs ~w(from_address_hash gas hash input nonce value)a @empty_attrs ~w()a @@ -83,6 +283,48 @@ defmodule Explorer.Chain.Transaction do """ @type wei_per_gas :: Wei.t() + @derive {Poison.Encoder, + only: [ + :block_number, + :block_timestamp, + :cumulative_gas_used, + :error, + :gas, + :gas_price, + :gas_used, + :index, + :created_contract_code_indexed_at, + :input, + :nonce, + :r, + :s, + :v, + :status, + :value, + :revert_reason + ]} + + @derive {Jason.Encoder, + only: [ + :block_number, + :block_timestamp, + :cumulative_gas_used, + :error, + :gas, + :gas_price, + :gas_used, + :index, + :created_contract_code_indexed_at, + :input, + :nonce, + :r, + :s, + :v, + :status, + :value, + :revert_reason + ]} + @typedoc """ * `block` - the block in which this transaction was mined/validated. `nil` when transaction is pending or has only been collated into one of the `uncles` in one of the `forks`. @@ -90,6 +332,8 @@ defmodule Explorer.Chain.Transaction do `uncles` in one of the `forks`. * `block_number` - Denormalized `block` `number`. `nil` when transaction is pending or has only been collated into one of the `uncles` in one of the `forks`. + * `block_consensus` - consensus of the block where transaction collated. + * `block_timestamp` - timestamp of the block where transaction collated. * `created_contract_address` - belongs_to association to `address` corresponding to `created_contract_address_hash`. * `created_contract_address_hash` - Denormalized `internal_transaction` `created_contract_address_hash` populated only when `to_address_hash` is nil. @@ -162,218 +406,7 @@ defmodule Explorer.Chain.Transaction do * `wrapped_s` - S field of the signature from the `wrapped` field (used by Suave) * `wrapped_hash` - hash from the `wrapped` field (used by Suave) """ - @type t :: - Map.merge( - %__MODULE__{ - block: %Ecto.Association.NotLoaded{} | Block.t() | nil, - block_hash: Hash.t() | nil, - block_number: Block.block_number() | nil, - created_contract_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - created_contract_address_hash: Hash.Address.t() | nil, - created_contract_code_indexed_at: DateTime.t() | nil, - cumulative_gas_used: Gas.t() | nil, - earliest_processing_start: DateTime.t() | nil, - error: String.t() | nil, - forks: %Ecto.Association.NotLoaded{} | [Fork.t()], - from_address: %Ecto.Association.NotLoaded{} | Address.t(), - from_address_hash: Hash.Address.t(), - gas: Gas.t(), - gas_price: wei_per_gas | nil, - gas_used: Gas.t() | nil, - hash: Hash.t(), - index: transaction_index | nil, - input: Data.t(), - internal_transactions: %Ecto.Association.NotLoaded{} | [InternalTransaction.t()], - logs: %Ecto.Association.NotLoaded{} | [Log.t()], - nonce: non_neg_integer(), - r: r(), - s: s(), - status: Status.t() | nil, - to_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - to_address_hash: Hash.Address.t() | nil, - uncles: %Ecto.Association.NotLoaded{} | [Block.t()], - v: v(), - value: Wei.t(), - revert_reason: String.t() | nil, - max_priority_fee_per_gas: wei_per_gas | nil, - max_fee_per_gas: wei_per_gas | nil, - type: non_neg_integer() | nil, - has_error_in_internal_txs: boolean(), - transaction_fee_log: any(), - transaction_fee_token: any() - }, - suave - ) - - if Application.compile_env(:explorer, :chain_type) == "suave" do - @type suave :: %{ - execution_node: %Ecto.Association.NotLoaded{} | Address.t() | nil, - execution_node_hash: Hash.Address.t() | nil, - wrapped_type: non_neg_integer() | nil, - wrapped_nonce: non_neg_integer() | nil, - wrapped_to_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, - wrapped_to_address_hash: Hash.Address.t() | nil, - wrapped_gas: Gas.t() | nil, - wrapped_gas_price: wei_per_gas | nil, - wrapped_max_priority_fee_per_gas: wei_per_gas | nil, - wrapped_max_fee_per_gas: wei_per_gas | nil, - wrapped_value: Wei.t() | nil, - wrapped_input: Data.t() | nil, - wrapped_v: v() | nil, - wrapped_r: r() | nil, - wrapped_s: s() | nil, - wrapped_hash: Hash.t() | nil - } - else - @type suave :: %{} - end - - @derive {Poison.Encoder, - only: [ - :block_number, - :cumulative_gas_used, - :error, - :gas, - :gas_price, - :gas_used, - :index, - :created_contract_code_indexed_at, - :input, - :nonce, - :r, - :s, - :v, - :status, - :value, - :revert_reason - ]} - - @derive {Jason.Encoder, - only: [ - :block_number, - :cumulative_gas_used, - :error, - :gas, - :gas_price, - :gas_used, - :index, - :created_contract_code_indexed_at, - :input, - :nonce, - :r, - :s, - :v, - :status, - :value, - :revert_reason - ]} - - @primary_key {:hash, Hash.Full, autogenerate: false} - schema "transactions" do - field(:block_number, :integer) - field(:cumulative_gas_used, :decimal) - field(:earliest_processing_start, :utc_datetime_usec) - field(:error, :string) - field(:gas, :decimal) - field(:gas_price, Wei) - field(:gas_used, :decimal) - field(:index, :integer) - field(:created_contract_code_indexed_at, :utc_datetime_usec) - field(:input, Data) - field(:nonce, :integer) - field(:r, :decimal) - field(:s, :decimal) - field(:status, Status) - field(:v, :decimal) - field(:value, Wei) - field(:revert_reason, :string) - field(:max_priority_fee_per_gas, Wei) - field(:max_fee_per_gas, Wei) - field(:type, :integer) - field(:has_error_in_internal_txs, :boolean) - field(:has_token_transfers, :boolean, virtual: true) - - # stability virtual fields - field(:transaction_fee_log, :any, virtual: true) - field(:transaction_fee_token, :any, virtual: true) - - # A transient field for deriving old block hash during transaction upserts. - # Used to force refetch of a block in case a transaction is re-collated - # in a different block. See: https://github.com/blockscout/blockscout/issues/1911 - field(:old_block_hash, Hash.Full) - - timestamps() - - belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full) - has_many(:forks, Fork, foreign_key: :hash) - - belongs_to( - :from_address, - Address, - foreign_key: :from_address_hash, - references: :hash, - type: Hash.Address - ) - - has_many(:internal_transactions, InternalTransaction, foreign_key: :transaction_hash) - has_many(:logs, Log, foreign_key: :transaction_hash) - has_many(:token_transfers, TokenTransfer, foreign_key: :transaction_hash) - has_many(:transaction_actions, TransactionAction, foreign_key: :hash, preload_order: [asc: :log_index]) - - belongs_to( - :to_address, - Address, - foreign_key: :to_address_hash, - references: :hash, - type: Hash.Address - ) - - has_many(:uncles, through: [:forks, :uncle]) - - has_one(:zkevm_batch_transaction, BatchTransaction, foreign_key: :hash) - has_one(:zkevm_batch, through: [:zkevm_batch_transaction, :batch]) - has_one(:zkevm_sequence_transaction, through: [:zkevm_batch, :sequence_transaction]) - has_one(:zkevm_verify_transaction, through: [:zkevm_batch, :verify_transaction]) - - belongs_to( - :created_contract_address, - Address, - foreign_key: :created_contract_address_hash, - references: :hash, - type: Hash.Address - ) - - if System.get_env("CHAIN_TYPE") == "suave" do - belongs_to( - :execution_node, - Address, - foreign_key: :execution_node_hash, - references: :hash, - type: Hash.Address - ) - - field(:wrapped_type, :integer) - field(:wrapped_nonce, :integer) - field(:wrapped_gas, :decimal) - field(:wrapped_gas_price, Wei) - field(:wrapped_max_priority_fee_per_gas, Wei) - field(:wrapped_max_fee_per_gas, Wei) - field(:wrapped_value, Wei) - field(:wrapped_input, Data) - field(:wrapped_v, :decimal) - field(:wrapped_r, :decimal) - field(:wrapped_s, :decimal) - field(:wrapped_hash, Hash.Full) - - belongs_to( - :wrapped_to_address, - Address, - foreign_key: :wrapped_to_address_hash, - references: :hash, - type: Hash.Address - ) - end - end + Explorer.Chain.Transaction.Schema.generate() @doc """ A pending transaction does not have a `block_hash` @@ -528,7 +561,7 @@ defmodule Explorer.Chain.Transaction do attrs_to_cast = @required_attrs ++ @optional_attrs ++ - if Application.get_env(:explorer, :chain_type) == "suave", do: @suave_optional_attrs, else: @empty_attrs + custom_optional_attrs() transaction |> cast(attrs, attrs_to_cast) @@ -543,6 +576,19 @@ defmodule Explorer.Chain.Transaction do |> unique_constraint(:hash) end + defp custom_optional_attrs do + case Application.get_env(:explorer, :chain_type) do + "suave" -> @suave_optional_attrs + "optimism" -> @optimism_optional_attrs + _ -> @empty_attrs + end + end + + @spec block_timestamp(t()) :: DateTime.t() + def block_timestamp(%{block_number: nil, inserted_at: time}), do: time + def block_timestamp(%{block_timestamp: time}) when not is_nil(time), do: time + def block_timestamp(%{block: %{timestamp: time}}), do: time + def preload_token_transfers(query, address_hash) do token_transfers_query = from( @@ -593,6 +639,18 @@ defmodule Explorer.Chain.Transaction do end # Because there is no contract association, we know the contract was not verified + @spec decoded_input_data( + NotLoaded.t() | Transaction.t(), + boolean(), + [Chain.api?()], + full_abi_acc, + methods_acc + ) :: + {error_type | success_type, full_abi_acc, methods_acc} + when full_abi_acc: map(), + methods_acc: map(), + error_type: {:error, any()} | {:error, :contract_not_verified | :contract_verified, list()}, + success_type: {:ok | binary(), any()} | {:ok, binary(), binary(), list()} def decoded_input_data(tx, skip_sig_provider? \\ false, options, full_abi_acc \\ %{}, methods_acc \\ %{}) def decoded_input_data(%__MODULE__{to_address: nil}, _, _, full_abi_acc, methods_acc), @@ -612,7 +670,7 @@ defmodule Explorer.Chain.Transaction do def decoded_input_data( %__MODULE__{ - to_address: %NotLoaded{}, + to_address: %{smart_contract: nil}, input: input, hash: hash }, @@ -623,7 +681,7 @@ defmodule Explorer.Chain.Transaction do ) do decoded_input_data( %__MODULE__{ - to_address: %{smart_contract: nil}, + to_address: %NotLoaded{}, input: input, hash: hash }, @@ -647,7 +705,7 @@ defmodule Explorer.Chain.Transaction do ) do decoded_input_data( %__MODULE__{ - to_address: %{smart_contract: nil}, + to_address: %NotLoaded{}, input: input, hash: hash }, @@ -660,7 +718,7 @@ defmodule Explorer.Chain.Transaction do def decoded_input_data( %__MODULE__{ - to_address: %{smart_contract: nil}, + to_address: %NotLoaded{}, input: %{bytes: <> = data} = input, hash: hash }, @@ -687,7 +745,7 @@ defmodule Explorer.Chain.Transaction do full_abi_acc, methods_acc} end - def decoded_input_data(%__MODULE__{to_address: %{smart_contract: nil}}, _, _, full_abi_acc, methods_acc) do + def decoded_input_data(%__MODULE__{to_address: %NotLoaded{}}, _, _, full_abi_acc, methods_acc) do {{:error, :contract_not_verified, []}, full_abi_acc, methods_acc} end @@ -707,7 +765,7 @@ defmodule Explorer.Chain.Transaction do {{:error, :could_not_decode}, full_abi_acc} -> case decoded_input_data( %__MODULE__{ - to_address: %{smart_contract: nil}, + to_address: %NotLoaded{}, input: input, hash: hash }, @@ -773,12 +831,7 @@ defmodule Explorer.Chain.Transaction do if Map.has_key?(methods_acc, method_id) do {methods_acc[method_id], methods_acc} else - candidates_query = - from( - contract_method in ContractMethod, - where: contract_method.identifier == ^method_id, - limit: 1 - ) + candidates_query = ContractMethod.find_contract_method_query(method_id, 1) result = candidates_query @@ -792,7 +845,7 @@ defmodule Explorer.Chain.Transaction do if !is_nil(address_hash) && Map.has_key?(full_abi_acc, address_hash) do {full_abi_acc[address_hash], full_abi_acc} else - full_abi = Chain.combine_proxy_implementation_abi(smart_contract, options) + full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, options) {full_abi, Map.put(full_abi_acc, address_hash, full_abi)} end @@ -808,7 +861,7 @@ defmodule Explorer.Chain.Transaction do else case decoded_input_data( %__MODULE__{ - to_address: %{smart_contract: nil}, + to_address: %NotLoaded{}, input: transaction.input, hash: transaction.hash }, @@ -1011,11 +1064,12 @@ defmodule Explorer.Chain.Transaction do """ def transactions_with_token_transfers(address_hash, token_hash) do query = transactions_with_token_transfers_query(address_hash, token_hash) + preloads = DenormalizationHelper.extend_block_preload([:from_address, :to_address, :created_contract_address]) from( t in subquery(query), order_by: [desc: t.block_number, desc: t.index], - preload: [:from_address, :to_address, :created_contract_address, :block] + preload: ^preloads ) end @@ -1032,11 +1086,12 @@ defmodule Explorer.Chain.Transaction do def transactions_with_token_transfers_direction(direction, address_hash) do query = transactions_with_token_transfers_query_direction(direction, address_hash) + preloads = DenormalizationHelper.extend_block_preload([:from_address, :to_address, :created_contract_address]) from( t in subquery(query), order_by: [desc: t.block_number, desc: t.index], - preload: [:from_address, :to_address, :created_contract_address, :block] + preload: ^preloads ) end @@ -1100,8 +1155,8 @@ defmodule Explorer.Chain.Transaction do @doc """ Returns true if the transaction is a Rootstock REMASC transaction. """ - @spec is_rootstock_remasc_transaction(Explorer.Chain.Transaction.t()) :: boolean - def is_rootstock_remasc_transaction(%__MODULE__{to_address_hash: to_address_hash}) do + @spec rootstock_remasc_transaction?(Explorer.Chain.Transaction.t()) :: boolean + def rootstock_remasc_transaction?(%__MODULE__{to_address_hash: to_address_hash}) do case Hash.Address.cast(Application.get_env(:explorer, __MODULE__)[:rootstock_remasc_address]) do {:ok, address} -> address == to_address_hash _ -> false @@ -1111,105 +1166,558 @@ defmodule Explorer.Chain.Transaction do @doc """ Returns true if the transaction is a Rootstock bridge transaction. """ - @spec is_rootstock_bridge_transaction(Explorer.Chain.Transaction.t()) :: boolean - def is_rootstock_bridge_transaction(%__MODULE__{to_address_hash: to_address_hash}) do + @spec rootstock_bridge_transaction?(Explorer.Chain.Transaction.t()) :: boolean + def rootstock_bridge_transaction?(%__MODULE__{to_address_hash: to_address_hash}) do case Hash.Address.cast(Application.get_env(:explorer, __MODULE__)[:rootstock_bridge_address]) do {:ok, address} -> address == to_address_hash _ -> false end end - @api_true [api?: true] - @transaction_fee_event_signature "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155" - @transaction_fee_event_abi [ - %{ - "anonymous" => false, - "inputs" => [ - %{ - "indexed" => false, - "internalType" => "address", - "name" => "token", - "type" => "address" - }, - %{ - "indexed" => false, - "internalType" => "uint256", - "name" => "totalFee", - "type" => "uint256" - }, - %{ - "indexed" => false, - "internalType" => "address", - "name" => "validator", - "type" => "address" - }, - %{ - "indexed" => false, - "internalType" => "uint256", - "name" => "validatorFee", - "type" => "uint256" - }, - %{ - "indexed" => false, - "internalType" => "address", - "name" => "dapp", - "type" => "address" - }, - %{ - "indexed" => false, - "internalType" => "uint256", - "name" => "dappFee", - "type" => "uint256" - } - ], - "name" => "TransactionFee", - "type" => "event" - } - ] + def bytes_to_address_hash(bytes), do: %Hash{byte_count: 20, bytes: bytes} - def maybe_prepare_stability_fees(transactions) do - if Application.get_env(:explorer, :chain_type) == "stability" do - maybe_prepare_stability_fees_inner(transactions) - else - transactions + @doc """ + Fetches the transactions related to the address with the given hash, including + transactions that only have the address in the `token_transfers` related table + and rewards for block validation. + + This query is divided into multiple subqueries intentionally in order to + improve the listing performance. + + The `token_transfers` table tends to grow exponentially, and the query results + with a `transactions` `join` statement takes too long. + + To solve this the `transaction_hashes` are fetched in a separate query, and + paginated through the `block_number` already present in the `token_transfers` + table. + + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the + `t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. + * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and + `:key` (a tuple of the lowest/oldest `{block_number, index}`) and. Results will be the transactions older than + the `block_number` and `index` that are passed. + + """ + @spec address_to_transactions_with_rewards(Hash.Address.t(), [ + Chain.paging_options() | Chain.necessity_by_association_option() + ]) :: [__MODULE__.t()] + def address_to_transactions_with_rewards(address_hash, options \\ []) when is_list(options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + case Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] && + Keyword.get(options, :direction) != :from && + Reward.address_has_rewards?(address_hash) && + Reward.get_validator_payout_key_by_mining_from_db(address_hash, options) do + %{payout_key: block_miner_payout_address} + when not is_nil(block_miner_payout_address) and address_hash == block_miner_payout_address -> + transactions_with_rewards_results(address_hash, options, paging_options) + + _ -> + address_to_transactions_without_rewards(address_hash, options) end end - defp maybe_prepare_stability_fees_inner(transactions) when is_list(transactions) do - {transactions, _tokens_acc} = - Enum.map_reduce(transactions, %{}, fn transaction, tokens_acc -> - case Log.fetch_log_by_tx_hash_and_first_topic(transaction.hash, @transaction_fee_event_signature, @api_true) do - fee_log when not is_nil(fee_log) -> - {:ok, _selector, mapping} = Log.find_and_decode(@transaction_fee_event_abi, fee_log, transaction) + defp transactions_with_rewards_results(address_hash, options, paging_options) do + blocks_range = address_to_transactions_tasks_range_of_blocks(address_hash, options) - [{"token", "address", false, token_address_hash}, _, _, _, _, _] = mapping + rewards_task = + Task.async(fn -> Reward.fetch_emission_rewards_tuples(address_hash, paging_options, blocks_range, options) end) - {token, new_tokens_acc} = check_tokens_acc(bytes_to_address_hash(token_address_hash), tokens_acc) + [rewards_task | address_to_transactions_tasks(address_hash, options, true)] + |> wait_for_address_transactions() + |> Enum.sort_by(fn item -> + case item do + {%Reward{} = emission_reward, _} -> + {-emission_reward.block.number, 1} - {%Transaction{transaction | transaction_fee_log: mapping, transaction_fee_token: token}, new_tokens_acc} + item -> + process_item(item) + end + end) + |> Enum.dedup_by(fn item -> + case item do + {%Reward{} = emission_reward, _} -> + {emission_reward.block_hash, emission_reward.address_hash, emission_reward.address_type} - _ -> - {transaction, tokens_acc} - end + transaction -> + transaction.hash + end + end) + |> Enum.take(paging_options.page_size) + end + + @doc false + def address_to_transactions_tasks_range_of_blocks(address_hash, options) do + extremums_list = + address_hash + |> transactions_block_numbers_at_address(options) + |> Enum.map(fn query -> + extremum_query = + from( + q in subquery(query), + select: %{min_block_number: min(q.block_number), max_block_number: max(q.block_number)} + ) + + extremum_query + |> Repo.one!() end) - transactions + extremums_list + |> Enum.reduce(%{min_block_number: nil, max_block_number: 0}, fn %{ + min_block_number: min_number, + max_block_number: max_number + }, + extremums_result -> + current_min_number = Map.get(extremums_result, :min_block_number) + current_max_number = Map.get(extremums_result, :max_block_number) + + extremums_result + |> process_extremums_result_against_min_number(current_min_number, min_number) + |> process_extremums_result_against_max_number(current_max_number, max_number) + end) end - defp maybe_prepare_stability_fees_inner(transaction) do - [transaction] = maybe_prepare_stability_fees_inner([transaction]) - transaction + defp transactions_block_numbers_at_address(address_hash, options) do + direction = Keyword.get(options, :direction) + + options + |> address_to_transactions_tasks_query(true) + |> not_pending_transactions() + |> select([t], t.block_number) + |> matching_address_queries_list(direction, address_hash) + end + + defp process_extremums_result_against_min_number(extremums_result, current_min_number, min_number) + when is_number(current_min_number) and + not (is_number(min_number) and min_number > 0 and min_number < current_min_number) do + extremums_result + end + + defp process_extremums_result_against_min_number(extremums_result, _current_min_number, min_number) do + extremums_result + |> Map.put(:min_block_number, min_number) + end + + defp process_extremums_result_against_max_number(extremums_result, current_max_number, max_number) + when is_number(max_number) and max_number > 0 and max_number > current_max_number do + extremums_result + |> Map.put(:max_block_number, max_number) + end + + defp process_extremums_result_against_max_number(extremums_result, _current_max_number, _max_number) do + extremums_result + end + + defp process_item(item) do + block_number = if item.block_number, do: -item.block_number, else: 0 + index = if item.index, do: -item.index, else: 0 + {block_number, index} + end + + @spec address_to_transactions_without_rewards( + Hash.Address.t(), + [ + Chain.paging_options() + | Chain.necessity_by_association_option() + | {:sorting, SortingHelper.sorting_params()} + ], + boolean() + ) :: [__MODULE__.t()] + def address_to_transactions_without_rewards(address_hash, options, old_ui? \\ true) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + address_hash + |> address_to_transactions_tasks(options, old_ui?) + |> wait_for_address_transactions() + |> Enum.sort(compare_custom_sorting(Keyword.get(options, :sorting, []))) + |> Enum.dedup_by(& &1.hash) + |> Enum.take(paging_options.page_size) + end + + defp address_to_transactions_tasks(address_hash, options, old_ui?) do + direction = Keyword.get(options, :direction) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + old_ui? = old_ui? || is_tuple(Keyword.get(options, :paging_options, Chain.default_paging_options()).key) + + options + |> address_to_transactions_tasks_query(false, old_ui?) + |> not_dropped_or_replaced_transactions() + |> Chain.join_associations(necessity_by_association) + |> put_has_token_transfers_to_tx(old_ui?) + |> matching_address_queries_list(direction, address_hash) + |> Enum.map(fn query -> Task.async(fn -> Chain.select_repo(options).all(query) end) end) + end + + @doc """ + Returns the address to transactions tasks query based on provided options. + Boolean `only_mined?` argument specifies if only mined transactions should be returned, + boolean `old_ui?` argument specifies if the query is for the old UI, i.e. is query dynamically sorted or no. + """ + @spec address_to_transactions_tasks_query(keyword, boolean, boolean) :: Ecto.Query.t() + def address_to_transactions_tasks_query(options, only_mined? \\ false, old_ui? \\ true) + + def address_to_transactions_tasks_query(options, only_mined?, true) do + from_block = Chain.from_block(options) + to_block = Chain.to_block(options) + + options + |> Keyword.get(:paging_options, Chain.default_paging_options()) + |> fetch_transactions(from_block, to_block, !only_mined?) + end + + def address_to_transactions_tasks_query(options, _only_mined?, false) do + from_block = Chain.from_block(options) + to_block = Chain.to_block(options) + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + sorting_options = Keyword.get(options, :sorting, []) + + fetch_transactions_with_custom_sorting(paging_options, from_block, to_block, sorting_options) + end + + @doc """ + Waits for the address transactions tasks to complete and returns the transactions flattened + in case of success or raises an error otherwise. + """ + @spec wait_for_address_transactions([Task.t()]) :: [__MODULE__.t()] + def wait_for_address_transactions(tasks) do + tasks + |> Task.yield_many(:timer.seconds(20)) + |> Enum.flat_map(fn {_task, res} -> + case res do + {:ok, result} -> + result + + {:exit, reason} -> + raise "Query fetching address transactions terminated: #{inspect(reason)}" + + nil -> + raise "Query fetching address transactions timed out." + end + end) + end + + defp compare_custom_sorting([{order, :value}]) do + fn a, b -> + case Decimal.compare(Wei.to(a.value, :wei), Wei.to(b.value, :wei)) do + :eq -> compare_default_sorting(a, b) + :gt -> order == :desc + :lt -> order == :asc + end + end + end + + defp compare_custom_sorting([{:dynamic, :fee, order, _dynamic_fee}]) do + fn a, b -> + nil_case = + case order do + :desc_nulls_last -> Decimal.new("-inf") + :asc_nulls_first -> Decimal.new("inf") + end + + a_fee = a |> fee(:wei) |> elem(1) || nil_case + b_fee = b |> fee(:wei) |> elem(1) || nil_case + + case Decimal.compare(a_fee, b_fee) do + :eq -> compare_default_sorting(a, b) + :gt -> order == :desc_nulls_last + :lt -> order == :asc_nulls_first + end + end + end + + defp compare_custom_sorting([]), do: &compare_default_sorting/2 + + defp compare_default_sorting(a, b) do + case { + compare(a.block_number, b.block_number), + compare(a.index, b.index), + DateTime.compare(a.inserted_at, b.inserted_at), + compare(Hash.to_integer(a.hash), Hash.to_integer(b.hash)) + } do + {:lt, _, _, _} -> false + {:eq, :lt, _, _} -> false + {:eq, :eq, :lt, _} -> false + {:eq, :eq, :eq, :gt} -> false + _ -> true + end + end + + defp compare(a, b) do + cond do + a < b -> :lt + a > b -> :gt + true -> :eq + end + end + + @doc """ + Creates a query to fetch transactions taking into account paging_options (possibly nil), + from_block (may be nil), to_block (may be nil) and boolean `with_pending?` that indicates if pending transactions should be included + into the query. + """ + @spec fetch_transactions(PagingOptions.t() | nil, non_neg_integer | nil, non_neg_integer | nil, boolean()) :: + Ecto.Query.t() + def fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil, with_pending? \\ false) do + __MODULE__ + |> order_for_transactions(with_pending?) + |> Chain.where_block_number_in_period(from_block, to_block) + |> handle_paging_options(paging_options) + end + + @default_sorting [ + desc: :block_number, + desc: :index, + desc: :inserted_at, + asc: :hash + ] + + @doc """ + Creates a query to fetch transactions taking into account paging_options (possibly nil), + from_block (may be nil), to_block (may be nil) and sorting_params. + """ + @spec fetch_transactions_with_custom_sorting( + PagingOptions.t() | nil, + non_neg_integer | nil, + non_neg_integer | nil, + SortingHelper.sorting_params() + ) :: Ecto.Query.t() + def fetch_transactions_with_custom_sorting(paging_options, from_block, to_block, sorting) do + query = from(transaction in __MODULE__) + + query + |> Chain.where_block_number_in_period(from_block, to_block) + |> SortingHelper.apply_sorting(sorting, @default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) + end + + defp order_for_transactions(query, true) do + query + |> order_by([transaction], + desc: transaction.block_number, + desc: transaction.index, + desc: transaction.inserted_at, + asc: transaction.hash + ) + end + + defp order_for_transactions(query, _) do + query + |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) + end + + @doc """ + Updates the provided query with necessary `where`s and `limit`s to take into account paging_options (may be nil). + """ + @spec handle_paging_options(Ecto.Query.t() | atom, nil | Explorer.PagingOptions.t()) :: Ecto.Query.t() + def handle_paging_options(query, nil), do: query + + def handle_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query + + def handle_paging_options(query, paging_options) do + query + |> page_transaction(paging_options) + |> limit(^paging_options.page_size) + end + + @doc """ + Updates the provided query with necessary `where`s to take into account paging_options. + """ + @spec page_transaction(Ecto.Query.t() | atom, Explorer.PagingOptions.t()) :: Ecto.Query.t() + def page_transaction(query, %PagingOptions{key: nil}), do: query + + def page_transaction(query, %PagingOptions{is_pending_tx: true} = options), + do: page_pending_transaction(query, options) + + def page_transaction(query, %PagingOptions{key: {block_number, index}, is_index_in_asc_order: true}) do + where( + query, + [transaction], + transaction.block_number < ^block_number or + (transaction.block_number == ^block_number and transaction.index > ^index) + ) + end + + def page_transaction(query, %PagingOptions{key: {block_number, index}}) do + where( + query, + [transaction], + transaction.block_number < ^block_number or + (transaction.block_number == ^block_number and transaction.index < ^index) + ) + end + + def page_transaction(query, %PagingOptions{key: {index}}) do + where(query, [transaction], transaction.index < ^index) + end + + @doc """ + Updates the provided query with necessary `where`s to take into account paging_options. + """ + @spec page_pending_transaction(Ecto.Query.t() | atom, Explorer.PagingOptions.t()) :: Ecto.Query.t() + def page_pending_transaction(query, %PagingOptions{key: nil}), do: query + + def page_pending_transaction(query, %PagingOptions{key: {inserted_at, hash}}) do + where( + query, + [transaction], + (is_nil(transaction.block_number) and + (transaction.inserted_at < ^inserted_at or + (transaction.inserted_at == ^inserted_at and transaction.hash > ^hash))) or + not is_nil(transaction.block_number) + ) + end + + @doc """ + Adds a `has_token_transfers` field to the query via `select_merge` if second argument is `false` and returns + the query untouched otherwise. + """ + @spec put_has_token_transfers_to_tx(Ecto.Query.t() | atom, boolean) :: Ecto.Query.t() + def put_has_token_transfers_to_tx(query, true), do: query + + def put_has_token_transfers_to_tx(query, false) do + from(tx in query, + select_merge: %{ + has_token_transfers: + fragment( + "(SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NOT NULL", + tx.hash + ) + } + ) + end + + @doc """ + Return the dynamic that calculates the fee for transactions. + """ + @spec dynamic_fee :: Ecto.Query.dynamic_expr() + def dynamic_fee do + dynamic([tx], tx.gas_price * fragment("COALESCE(?, ?)", tx.gas_used, tx.gas)) end - defp check_tokens_acc(token_address_hash, tokens_acc) do - if Map.has_key?(tokens_acc, token_address_hash) do - {tokens_acc[token_address_hash], tokens_acc} + @doc """ + Returns next page params based on the provided transaction. + """ + @spec address_transactions_next_page_params(Explorer.Chain.Transaction.t()) :: %{ + required(String.t()) => Decimal.t() | Wei.t() | non_neg_integer | DateTime.t() | Hash.t() + } + def address_transactions_next_page_params( + %__MODULE__{block_number: block_number, index: index, inserted_at: inserted_at, hash: hash, value: value} = tx + ) do + %{ + "fee" => tx |> fee(:wei) |> elem(1), + "value" => value, + "block_number" => block_number, + "index" => index, + "inserted_at" => inserted_at, + "hash" => hash + } + end + + @doc """ + The fee a `transaction` paid for the `t:Explorer.Transaction.t/0` `gas` + + If the transaction is pending, then the fee will be a range of `unit` + + iex> Explorer.Chain.Transaction.fee( + ...> %Explorer.Chain.Transaction{ + ...> gas: Decimal.new(3), + ...> gas_price: %Explorer.Chain.Wei{value: Decimal.new(2)}, + ...> gas_used: nil + ...> }, + ...> :wei + ...> ) + {:maximum, Decimal.new(6)} + + If the transaction has been confirmed in block, then the fee will be the actual fee paid in `unit` for the `gas_used` + in the `transaction`. + + iex> Explorer.Chain.Transaction.fee( + ...> %Explorer.Chain.Transaction{ + ...> gas: Decimal.new(3), + ...> gas_price: %Explorer.Chain.Wei{value: Decimal.new(2)}, + ...> gas_used: Decimal.new(2) + ...> }, + ...> :wei + ...> ) + {:actual, Decimal.new(4)} + + """ + @spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t()} | {:actual, Decimal.t() | nil} + def fee(%Transaction{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil} + + def fee(%Transaction{gas: gas, gas_price: gas_price, gas_used: nil} = tx, unit) do + {:maximum, fee(tx, gas_price, gas, unit)} + end + + def fee(%Transaction{gas_price: nil, gas_used: gas_used} = transaction, unit) do + if Application.get_env(:explorer, :chain_type) == "optimism" do + {:actual, nil} else - token = Token.get_by_contract_address_hash(token_address_hash, @api_true) + gas_price = effective_gas_price(transaction) - {token, Map.put(tokens_acc, token_address_hash, token)} + {:actual, + gas_price && + gas_price + |> Wei.to(unit) + |> Decimal.mult(gas_used)} end end - def bytes_to_address_hash(bytes), do: %Hash{byte_count: 20, bytes: bytes} + def fee(%Transaction{gas_price: gas_price, gas_used: gas_used} = tx, unit) do + {:actual, fee(tx, gas_price, gas_used, unit)} + end + + defp fee(tx, gas_price, gas, unit) do + l1_fee = + case Map.get(tx, :l1_fee) do + nil -> Wei.from(Decimal.new(0), :wei) + value -> value + end + + gas_price + |> Wei.to(unit) + |> Decimal.mult(gas) + |> Wei.from(unit) + |> Wei.sum(l1_fee) + |> Wei.to(unit) + end + + @doc """ + Calculates effective gas price for transaction with type 2 (EIP-1559) + + `effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas` + """ + @spec effective_gas_price(Transaction.t()) :: Wei.t() | nil + + def effective_gas_price(%Transaction{block: nil}), do: nil + def effective_gas_price(%Transaction{block: %NotLoaded{}}), do: nil + + def effective_gas_price(%Transaction{} = transaction) do + base_fee_per_gas = transaction.block.base_fee_per_gas + max_priority_fee_per_gas = transaction.max_priority_fee_per_gas + max_fee_per_gas = transaction.max_fee_per_gas + + priority_fee_per_gas = priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) + + priority_fee_per_gas && Wei.sum(priority_fee_per_gas, base_fee_per_gas) + end + + @doc """ + Calculates priority fee per gas for transaction with type 2 (EIP-1559) + + `priority_fee_per_gas = min(transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas - block.base_fee_per_gas)` + """ + @spec priority_fee_per_gas(Wei.t() | nil, Wei.t() | nil, Wei.t() | nil) :: Wei.t() | nil + def priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) do + if is_nil(max_priority_fee_per_gas) or is_nil(base_fee_per_gas), + do: nil, + else: + max_priority_fee_per_gas + |> Wei.to(:wei) + |> Decimal.min(max_fee_per_gas |> Wei.sub(base_fee_per_gas) |> Wei.to(:wei)) + |> Wei.from(:wei) + end end diff --git a/apps/explorer/lib/explorer/chain/transaction/fork.ex b/apps/explorer/lib/explorer/chain/transaction/fork.ex index 74e2fc921d35..794d64c17360 100644 --- a/apps/explorer/lib/explorer/chain/transaction/fork.ex +++ b/apps/explorer/lib/explorer/chain/transaction/fork.ex @@ -20,22 +20,14 @@ defmodule Explorer.Chain.Transaction.Fork do * `uncle` - the block in which this transaction was mined/validated. * `uncle_hash` - `uncle` foreign key. """ - @type t :: %__MODULE__{ - hash: Hash.t(), - index: Transaction.transaction_index(), - transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), - uncle: %Ecto.Association.NotLoaded{} | Block.t(), - uncle_hash: Hash.t() - } - @primary_key false - schema "transaction_forks" do - field(:index, :integer) + typed_schema "transaction_forks" do + field(:index, :integer, null: false) timestamps() - belongs_to(:transaction, Transaction, foreign_key: :hash, references: :hash, type: Hash.Full) - belongs_to(:uncle, Block, foreign_key: :uncle_hash, references: :hash, type: Hash.Full) + belongs_to(:transaction, Transaction, foreign_key: :hash, references: :hash, type: Hash.Full, null: false) + belongs_to(:uncle, Block, foreign_key: :uncle_hash, references: :hash, type: Hash.Full, null: false) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex index d828cb63e794..de731e61c872 100644 --- a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex +++ b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex @@ -6,7 +6,7 @@ defmodule Explorer.Chain.Transaction.History.Historian do use Explorer.History.Historian alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.{Block, DenormalizationHelper, Transaction} alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.History.Process, as: HistoryProcess @@ -89,25 +89,38 @@ defmodule Explorer.Chain.Transaction.History.Historian do Logger.info("tx/per day chart: min/max block numbers [#{min_block}, #{max_block}]") all_transactions_query = - from( - transaction in Transaction, - where: transaction.block_number >= ^min_block and transaction.block_number <= ^max_block - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + transaction in Transaction, + where: transaction.block_number >= ^min_block and transaction.block_number <= ^max_block, + where: transaction.block_consensus == true, + select: transaction + ) + else + from( + transaction in Transaction, + where: transaction.block_number >= ^min_block and transaction.block_number <= ^max_block + ) + end all_blocks_query = from( block in Block, where: block.consensus == true, where: block.number >= ^min_block and block.number <= ^max_block, - select: block.hash + select: block.number ) query = - from(transaction in subquery(all_transactions_query), - join: block in subquery(all_blocks_query), - on: transaction.block_hash == block.hash, - select: transaction - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + all_transactions_query + else + from(transaction in subquery(all_transactions_query), + join: block in subquery(all_blocks_query), + on: transaction.block_number == block.number, + select: transaction + ) + end num_transactions = Repo.aggregate(query, :count, :hash, timeout: :infinity) Logger.info("tx/per day chart: num of transactions #{num_transactions}") @@ -115,11 +128,18 @@ defmodule Explorer.Chain.Transaction.History.Historian do Logger.info("tx/per day chart: total gas used #{gas_used}") total_fee_query = - from(transaction in subquery(all_transactions_query), - join: block in subquery(all_blocks_query), - on: transaction.block_hash == block.hash, - select: fragment("SUM(? * ?)", transaction.gas_price, transaction.gas_used) - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from(transaction in subquery(all_transactions_query), + select: fragment("SUM(? * ?)", transaction.gas_price, transaction.gas_used) + ) + else + from(transaction in subquery(all_transactions_query), + join: block in Block, + on: transaction.block_hash == block.hash, + where: block.consensus == true, + select: fragment("SUM(? * ?)", transaction.gas_price, transaction.gas_used) + ) + end total_fee = Repo.one(total_fee_query, timeout: :infinity) Logger.info("tx/per day chart: total fee #{total_fee}") diff --git a/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex b/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex index 9d8363ae676e..594d66bbae4a 100644 --- a/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex +++ b/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex @@ -14,13 +14,6 @@ defmodule Explorer.Chain.Transaction.History.TransactionStats do :__meta__ ]} - schema "transaction_stats" do - field(:date, :date) - field(:number_of_transactions, :integer) - field(:gas_used, :decimal) - field(:total_fee, :decimal) - end - @typedoc """ The recorded values of the number of transactions for a single day. * `:date` - The date in UTC. @@ -28,12 +21,12 @@ defmodule Explorer.Chain.Transaction.History.TransactionStats do * `:gas_used` - Gas used in transactions per single day * `:total_fee` - Total fee paid to validators from success transactions per single day """ - @type t :: %__MODULE__{ - date: Date.t(), - number_of_transactions: integer(), - gas_used: non_neg_integer(), - total_fee: non_neg_integer() - } + typed_schema "transaction_stats" do + field(:date, :date) + field(:number_of_transactions, :integer) + field(:gas_used, :decimal) + field(:total_fee, :decimal) + end @spec by_date_range(Date.t(), Date.t()) :: [__MODULE__] def by_date_range(earliest, latest, options \\ []) do diff --git a/apps/explorer/lib/explorer/chain/transaction/state_change.ex b/apps/explorer/lib/explorer/chain/transaction/state_change.ex index 3a399aa019f1..3d5f0f7f8b5e 100644 --- a/apps/explorer/lib/explorer/chain/transaction/state_change.ex +++ b/apps/explorer/lib/explorer/chain/transaction/state_change.ex @@ -4,7 +4,7 @@ defmodule Explorer.Chain.Transaction.StateChange do """ alias Explorer.Chain - alias Explorer.Chain.{Hash, TokenTransfer, Wei} + alias Explorer.Chain.{Hash, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Transaction.StateChange defstruct [:coin_or_token_transfers, :address, :token_id, :balance_before, :balance_after, :balance_diff, :miner?] @@ -120,7 +120,7 @@ defmodule Explorer.Chain.Transaction.StateChange do end defp do_update_balance(old_val, type, transfer, _) do - token_ids = if transfer.token.type == "ERC-1155", do: transfer.token_ids || [transfer.token_id], else: [nil] + token_ids = if transfer.token.type == "ERC-1155", do: transfer.token_ids, else: [nil] transfer_amounts = transfer.amounts || [transfer.amount || 1] sub_or_add = @@ -140,7 +140,7 @@ defmodule Explorer.Chain.Transaction.StateChange do end def from_loss(tx) do - {_, fee} = Chain.fee(tx, :wei) + {_, fee} = Transaction.fee(tx, :wei) if error?(tx) do %Wei{value: fee} diff --git a/apps/explorer/lib/explorer/chain/transaction_action.ex b/apps/explorer/lib/explorer/chain/transaction_action.ex index a280d85fefa5..a8aba0bc5a59 100644 --- a/apps/explorer/lib/explorer/chain/transaction_action.ex +++ b/apps/explorer/lib/explorer/chain/transaction_action.ex @@ -18,18 +18,10 @@ defmodule Explorer.Chain.TransactionAction do * `type` - type of the action protocol (see possible values for Enum of the db table field) * `log_index` - index of the action for sorting (taken from log.index) """ - @type t :: %__MODULE__{ - hash: Hash.t(), - protocol: String.t(), - data: map(), - type: String.t(), - log_index: non_neg_integer() - } - @primary_key false - schema "transaction_actions" do - field(:protocol, Ecto.Enum, values: @supported_protocols) - field(:data, :map) + typed_schema "transaction_actions" do + field(:protocol, Ecto.Enum, values: @supported_protocols, null: false) + field(:data, :map, null: false) field(:type, Ecto.Enum, values: [ @@ -54,12 +46,19 @@ defmodule Explorer.Chain.TransactionAction do :enable_collateral, :disable_collateral, :liquidation_call - ] + ], + null: false ) - field(:log_index, :integer, primary_key: true) + field(:log_index, :integer, primary_key: true, null: false) - belongs_to(:transaction, Transaction, foreign_key: :hash, primary_key: true, references: :hash, type: Hash.Full) + belongs_to(:transaction, Transaction, + foreign_key: :hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/user_operation.ex b/apps/explorer/lib/explorer/chain/user_operation.ex new file mode 100644 index 000000000000..cdcb322e964d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/user_operation.ex @@ -0,0 +1,66 @@ +defmodule Explorer.Chain.UserOperation do + @moduledoc """ + The representation of a user operation for account abstraction (EIP-4337). + """ + + require Logger + + import Ecto.Query, + only: [ + where: 2 + ] + + use Explorer.Schema + alias Explorer.Chain + alias Explorer.Chain.Hash + alias Explorer.Utility.Microservice + + @type api? :: {:api?, true | false} + + @typedoc """ + * `hash` - the hash of User operation. + * `block_number` - the block number, where user operation happened. + * `block_hash` - the block hash, where user operation happened. + """ + @primary_key false + typed_schema "user_operations" do + field(:hash, Hash.Full, primary_key: true, null: false) + field(:block_number, :integer, null: false) + field(:block_hash, Hash.Full, null: false) + + timestamps() + end + + def changeset(%__MODULE__{} = user_operation, attrs) do + user_operation + |> cast(attrs, [ + :hash, + :block_number, + :block_hash + ]) + |> validate_required([:hash, :block_number, :block_hash]) + end + + @doc """ + Converts `t:Explorer.Chain.UserOperation.t/0` `hash` to the `t:Explorer.Chain.UserOperation.t/0` with that `hash`. + """ + @spec hash_to_user_operation(Hash.Full.t(), [api?]) :: + {:ok, __MODULE__.t()} | {:error, :not_found} + def hash_to_user_operation(%Hash{byte_count: unquote(Hash.Full.byte_count())} = hash, options \\ []) + when is_list(options) do + __MODULE__ + |> where(hash: ^hash) + |> Chain.select_repo(options).one() + |> case do + nil -> + {:error, :not_found} + + user_operation -> + {:ok, user_operation} + end + end + + def enabled? do + Microservice.check_enabled(Explorer.MicroserviceInterfaces.AccountAbstraction) == :ok + end +end diff --git a/apps/explorer/lib/explorer/chain/validator.ex b/apps/explorer/lib/explorer/chain/validator.ex index 14c416a1f8f2..76447d851921 100644 --- a/apps/explorer/lib/explorer/chain/validator.ex +++ b/apps/explorer/lib/explorer/chain/validator.ex @@ -8,8 +8,8 @@ defmodule Explorer.Chain.Validator do alias Explorer.{Chain, Repo} @primary_key false - schema "validators" do - field(:address_hash, Address, primary_key: true) + typed_schema "validators" do + field(:address_hash, Address, primary_key: true, null: false) field(:is_validator, :boolean) field(:payout_key_hash, Address) field(:info_updated_at_block, :integer) diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 533174ba4524..8fba9b05ef0a 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -20,6 +20,7 @@ defmodule Explorer.Chain.Wei do """ + require Decimal alias Explorer.Chain.Wei defstruct ~w(value)a @@ -117,8 +118,12 @@ defmodule Explorer.Chain.Wei do @wei_per_ether Decimal.new(1_000_000_000_000_000_000) @wei_per_gwei Decimal.new(1_000_000_000) - @spec hex_format(Wei.t()) :: String.t() + @spec hex_format(Wei.t() | Decimal.t()) :: String.t() def hex_format(%Wei{value: decimal}) do + hex_format(decimal) + end + + def hex_format(%Decimal{} = decimal) do hex = decimal |> Decimal.to_integer() @@ -138,13 +143,24 @@ defmodule Explorer.Chain.Wei do iex> Explorer.Chain.Wei.sum(first, second) %Explorer.Chain.Wei{value: Decimal.new(1_123)} """ - @spec sum(Wei.t(), Wei.t()) :: Wei.t() + @spec sum(Wei.t() | nil, Wei.t() | nil) :: Wei.t() | nil + def sum(%Wei{value: wei_1}, %Wei{value: nil}) do + wei_1 + |> from(:wei) + end + + def sum(%Wei{value: nil}, %Wei{value: wei_2}) do + wei_2 + |> from(:wei) + end + def sum(%Wei{value: wei_1}, %Wei{value: wei_2}) do wei_1 |> Decimal.add(wei_2) |> from(:wei) end + @spec sub(Wei.t(), Wei.t()) :: Wei.t() | nil @doc """ Subtracts two Wei values. @@ -155,6 +171,8 @@ defmodule Explorer.Chain.Wei do iex> Explorer.Chain.Wei.sub(first, second) %Explorer.Chain.Wei{value: Decimal.new(123)} """ + def sub(_, nil), do: nil + def sub(%Wei{value: wei_1}, %Wei{value: wei_2}) do wei_1 |> Decimal.sub(wei_2) @@ -183,6 +201,29 @@ defmodule Explorer.Chain.Wei do |> from(:wei) end + @doc """ + Divides Wei values by an `t:integer/0` or `t:Decimal.t/0`. + + ## Example + + iex> wei = %Explorer.Chain.Wei{value: Decimal.new(10)} + iex> divisor = 5 + iex> Explorer.Chain.Wei.div(wei, divisor) + %Explorer.Chain.Wei{value: Decimal.new(2)} + """ + @spec div(t(), pos_integer() | Decimal.t()) :: t() + def div(%Wei{value: value}, divisor) when is_integer(divisor) and divisor > 0 do + value + |> Decimal.div(divisor) + |> from(:wei) + end + + def div(%Wei{value: value}, %Decimal{sign: 1} = divisor) do + value + |> Decimal.div(divisor) + |> from(:wei) + end + @doc """ Converts `Decimal` representations of various wei denominations (wei, Gwei, ether) to a wei base unit. @@ -206,17 +247,23 @@ defmodule Explorer.Chain.Wei do """ - @spec from(ether(), :ether) :: t() + @spec from(ether() | nil, :ether) :: t() | nil + def from(nil, :ether), do: nil + def from(%Decimal{} = ether, :ether) do %__MODULE__{value: Decimal.mult(ether, @wei_per_ether)} end - @spec from(gwei(), :gwei) :: t() + @spec from(gwei(), :gwei) :: t() | nil + def from(nil, :gwei), do: nil + def from(%Decimal{} = gwei, :gwei) do %__MODULE__{value: Decimal.mult(gwei, @wei_per_gwei)} end @spec from(wei(), :wei) :: t() + def from(nil, :wei), do: nil + def from(%Decimal{} = wei, :wei) do %__MODULE__{value: wei} end @@ -247,17 +294,22 @@ defmodule Explorer.Chain.Wei do """ - @spec to(t(), :ether) :: ether() + @spec to(t(), :ether) :: ether() | nil + def to(nil, :ether), do: nil + def to(%__MODULE__{value: wei}, :ether) do Decimal.div(wei, @wei_per_ether) end - @spec to(t(), :gwei) :: gwei() + @spec to(t(), :gwei) :: gwei() | nil + def to(nil, :gwei), do: nil + def to(%__MODULE__{value: wei}, :gwei) do Decimal.div(wei, @wei_per_gwei) end - @spec to(t(), :wei) :: wei() + @spec to(t(), :wei) :: wei() | nil + def to(nil, :wei), do: nil def to(%__MODULE__{value: wei}, :wei), do: wei end diff --git a/apps/explorer/lib/explorer/chain/withdrawal.ex b/apps/explorer/lib/explorer/chain/withdrawal.ex index 250307c5670c..ecba16f92ffd 100644 --- a/apps/explorer/lib/explorer/chain/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/withdrawal.ex @@ -8,33 +8,26 @@ defmodule Explorer.Chain.Withdrawal do alias Explorer.Chain.{Address, Block, Hash, Wei} alias Explorer.PagingOptions - @type t :: %__MODULE__{ - index: non_neg_integer(), - validator_index: non_neg_integer(), - amount: Wei.t(), - block: %Ecto.Association.NotLoaded{} | Block.t(), - block_hash: Hash.Full.t(), - address: %Ecto.Association.NotLoaded{} | Address.t(), - address_hash: Hash.Address.t() - } - @required_attrs ~w(index validator_index amount address_hash block_hash)a - @primary_key {:index, :integer, autogenerate: false} - schema "withdrawals" do - field(:validator_index, :integer) - field(:amount, Wei) + @primary_key false + typed_schema "withdrawals" do + field(:index, :integer, primary_key: true, null: false) + field(:validator_index, :integer, null: false) + field(:amount, Wei, null: false) belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false ) belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, - type: Hash.Full + type: Hash.Full, + null: false ) timestamps() diff --git a/apps/explorer/lib/explorer/chain/zkevm/reader.ex b/apps/explorer/lib/explorer/chain/zkevm/reader.ex deleted file mode 100644 index 49f69eaa0d46..000000000000 --- a/apps/explorer/lib/explorer/chain/zkevm/reader.ex +++ /dev/null @@ -1,149 +0,0 @@ -defmodule Explorer.Chain.Zkevm.Reader do - @moduledoc "Contains read functions for zkevm modules." - - import Ecto.Query, - only: [ - from: 2, - limit: 2, - order_by: 2, - where: 2, - where: 3 - ] - - import Explorer.Chain, only: [select_repo: 1] - - alias Explorer.Chain.Zkevm.{BatchTransaction, LifecycleTransaction, TransactionBatch} - alias Explorer.{Chain, PagingOptions, Repo} - - @doc """ - Reads a batch by its number from database. - If the number is :latest, gets the latest batch from `zkevm_transaction_batches` table. - Returns {:error, :not_found} in case the batch is not found. - """ - @spec batch(non_neg_integer() | :latest, list()) :: {:ok, map()} | {:error, :not_found} - def batch(number, options \\ []) - - def batch(:latest, options) when is_list(options) do - TransactionBatch - |> order_by(desc: :number) - |> limit(1) - |> select_repo(options).one() - |> case do - nil -> {:error, :not_found} - batch -> {:ok, batch} - end - end - - def batch(number, options) when is_list(options) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - - TransactionBatch - |> where(number: ^number) - |> Chain.join_associations(necessity_by_association) - |> select_repo(options).one() - |> case do - nil -> {:error, :not_found} - batch -> {:ok, batch} - end - end - - @doc """ - Reads a list of batches from `zkevm_transaction_batches` table. - """ - @spec batches(list()) :: list() - def batches(options \\ []) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - - base_query = - from(tb in TransactionBatch, - order_by: [desc: tb.number] - ) - - query = - if Keyword.get(options, :confirmed?, false) do - base_query - |> Chain.join_associations(necessity_by_association) - |> where([tb], not is_nil(tb.sequence_id) and tb.sequence_id > 0) - |> limit(10) - else - paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) - - base_query - |> Chain.join_associations(necessity_by_association) - |> page_batches(paging_options) - |> limit(^paging_options.page_size) - end - - select_repo(options).all(query) - end - - @doc """ - Reads a list of L2 transaction hashes from `zkevm_batch_l2_transactions` table. - """ - @spec batch_transactions(non_neg_integer(), list()) :: list() - def batch_transactions(batch_number, options \\ []) do - query = from(bts in BatchTransaction, where: bts.batch_number == ^batch_number) - - select_repo(options).all(query) - end - - @doc """ - Gets the number of the latest batch with defined verify_id from `zkevm_transaction_batches` table. - Returns 0 if not found. - """ - @spec last_verified_batch_number() :: non_neg_integer() - def last_verified_batch_number do - query = - from(tb in TransactionBatch, - select: tb.number, - where: not is_nil(tb.verify_id), - order_by: [desc: tb.number], - limit: 1 - ) - - query - |> Repo.one() - |> Kernel.||(0) - end - - @doc """ - Reads a list of L1 transactions by their hashes from `zkevm_lifecycle_l1_transactions` table. - """ - @spec lifecycle_transactions(list()) :: list() - def lifecycle_transactions(l1_tx_hashes) do - query = - from( - lt in LifecycleTransaction, - select: {lt.hash, lt.id}, - where: lt.hash in ^l1_tx_hashes - ) - - Repo.all(query, timeout: :infinity) - end - - @doc """ - Determines ID of the future lifecycle transaction by reading `zkevm_lifecycle_l1_transactions` table. - """ - @spec next_id() :: non_neg_integer() - def next_id do - query = - from(lt in LifecycleTransaction, - select: lt.id, - order_by: [desc: lt.id], - limit: 1 - ) - - last_id = - query - |> Repo.one() - |> Kernel.||(0) - - last_id + 1 - end - - defp page_batches(query, %PagingOptions{key: nil}), do: query - - defp page_batches(query, %PagingOptions{key: {number}}) do - from(tb in query, where: tb.number < ^number) - end -end diff --git a/apps/explorer/lib/explorer/chain/zksync/batch_block.ex b/apps/explorer/lib/explorer/chain/zksync/batch_block.ex new file mode 100644 index 000000000000..08c9be6912d0 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/zksync/batch_block.ex @@ -0,0 +1,37 @@ +defmodule Explorer.Chain.ZkSync.BatchBlock do + @moduledoc "Models a list of blocks related to a batch for ZkSync." + + use Explorer.Schema + + alias Explorer.Chain.{Block, Hash} + alias Explorer.Chain.ZkSync.TransactionBatch + + @required_attrs ~w(batch_number hash)a + + @type t :: %__MODULE__{ + batch_number: non_neg_integer(), + batch: %Ecto.Association.NotLoaded{} | TransactionBatch.t() | nil, + hash: Hash.t(), + block: %Ecto.Association.NotLoaded{} | Block.t() | nil + } + + @primary_key false + schema "zksync_batch_l2_blocks" do + belongs_to(:batch, TransactionBatch, foreign_key: :batch_number, references: :number, type: :integer) + belongs_to(:block, Block, foreign_key: :hash, primary_key: true, references: :hash, type: Hash.Full) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = items, attrs \\ %{}) do + items + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:batch_number) + |> unique_constraint(:hash) + end +end diff --git a/apps/explorer/lib/explorer/chain/zkevm/batch_transaction.ex b/apps/explorer/lib/explorer/chain/zksync/batch_transaction.ex similarity index 87% rename from apps/explorer/lib/explorer/chain/zkevm/batch_transaction.ex rename to apps/explorer/lib/explorer/chain/zksync/batch_transaction.ex index 74f5fb9b9f86..ef3cfb0af8e5 100644 --- a/apps/explorer/lib/explorer/chain/zkevm/batch_transaction.ex +++ b/apps/explorer/lib/explorer/chain/zksync/batch_transaction.ex @@ -1,10 +1,10 @@ -defmodule Explorer.Chain.Zkevm.BatchTransaction do - @moduledoc "Models a list of transactions related to a batch for zkEVM." +defmodule Explorer.Chain.ZkSync.BatchTransaction do + @moduledoc "Models a list of transactions related to a batch for ZkSync." use Explorer.Schema alias Explorer.Chain.{Hash, Transaction} - alias Explorer.Chain.Zkevm.TransactionBatch + alias Explorer.Chain.ZkSync.TransactionBatch @required_attrs ~w(batch_number hash)a @@ -16,7 +16,7 @@ defmodule Explorer.Chain.Zkevm.BatchTransaction do } @primary_key false - schema "zkevm_batch_l2_transactions" do + schema "zksync_batch_l2_transactions" do belongs_to(:batch, TransactionBatch, foreign_key: :batch_number, references: :number, type: :integer) belongs_to(:l2_transaction, Transaction, foreign_key: :hash, primary_key: true, references: :hash, type: Hash.Full) diff --git a/apps/explorer/lib/explorer/chain/zksync/lifecycle_transaction.ex b/apps/explorer/lib/explorer/chain/zksync/lifecycle_transaction.ex new file mode 100644 index 000000000000..cc2ec207a6a2 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/zksync/lifecycle_transaction.ex @@ -0,0 +1,38 @@ +defmodule Explorer.Chain.ZkSync.LifecycleTransaction do + @moduledoc "Models an L1 lifecycle transaction for ZkSync." + + use Explorer.Schema + + alias Explorer.Chain.Hash + alias Explorer.Chain.ZkSync.TransactionBatch + + @required_attrs ~w(id hash timestamp)a + + @type t :: %__MODULE__{ + hash: Hash.t(), + timestamp: DateTime.t() + } + + @primary_key {:id, :integer, autogenerate: false} + schema "zksync_lifecycle_l1_transactions" do + field(:hash, Hash.Full) + field(:timestamp, :utc_datetime_usec) + + has_many(:committed_batches, TransactionBatch, foreign_key: :commit_id) + has_many(:proven_batches, TransactionBatch, foreign_key: :prove_id) + has_many(:executed_batches, TransactionBatch, foreign_key: :execute_id) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = txn, attrs \\ %{}) do + txn + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:id) + end +end diff --git a/apps/explorer/lib/explorer/chain/zksync/reader.ex b/apps/explorer/lib/explorer/chain/zksync/reader.ex new file mode 100644 index 000000000000..2240cb23722e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/zksync/reader.ex @@ -0,0 +1,339 @@ +defmodule Explorer.Chain.ZkSync.Reader do + @moduledoc "Contains read functions for zksync modules." + + import Ecto.Query, + only: [ + from: 2, + limit: 2, + order_by: 2, + where: 2, + where: 3 + ] + + import Explorer.Chain, only: [select_repo: 1] + + alias Explorer.Chain.ZkSync.{ + BatchTransaction, + LifecycleTransaction, + TransactionBatch + } + + alias Explorer.{Chain, PagingOptions, Repo} + + @doc """ + Receives total amount of batches imported to the `zksync_transaction_batches` table. + + ## Parameters + - `options`: passed to `Chain.select_repo()` + + ## Returns + Total amount of batches + """ + @spec batches_count(keyword()) :: any() + def batches_count(options) do + TransactionBatch + |> select_repo(options).aggregate(:count, timeout: :infinity) + end + + @doc """ + Receives the batch from the `zksync_transaction_batches` table by using its number or the latest batch if `:latest` is used. + + ## Parameters + - `number`: could be either the batch number or `:latest` to get the latest available in DB batch + - `options`: passed to `Chain.select_repo()` + + ## Returns + - `{:ok, Explorer.Chain.ZkSync.TransactionBatch}` if the batch found + - `{:error, :not_found}` if there is no batch with such number + """ + @spec batch(:latest | binary() | integer(), keyword()) :: + {:error, :not_found} | {:ok, Explorer.Chain.ZkSync.TransactionBatch} + def batch(number, options) + + def batch(:latest, options) when is_list(options) do + TransactionBatch + |> order_by(desc: :number) + |> limit(1) + |> select_repo(options).one() + |> case do + nil -> {:error, :not_found} + batch -> {:ok, batch} + end + end + + def batch(number, options) + when (is_integer(number) or is_binary(number)) and + is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + TransactionBatch + |> where(number: ^number) + |> Chain.join_associations(necessity_by_association) + |> select_repo(options).one() + |> case do + nil -> {:error, :not_found} + batch -> {:ok, batch} + end + end + + @doc """ + Receives a list of batches from the `zksync_transaction_batches` table within the range of batch numbers + + ## Parameters + - `start_number`: The start of the batch numbers range. + - `end_number`: The end of the batch numbers range. + - `options`: Options passed to `Chain.select_repo()`. + + ## Returns + - A list of `Explorer.Chain.ZkSync.TransactionBatch` if at least one batch exists within the range. + - An empty list (`[]`) if no batches within the range are found in the database. + """ + @spec batches(integer(), integer(), keyword()) :: [Explorer.Chain.ZkSync.TransactionBatch] + def batches(start_number, end_number, options) + when is_integer(start_number) and + is_integer(end_number) and + is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + base_query = from(tb in TransactionBatch, order_by: [desc: tb.number]) + + base_query + |> where([tb], tb.number >= ^start_number and tb.number <= ^end_number) + |> Chain.join_associations(necessity_by_association) + |> select_repo(options).all() + end + + @doc """ + Receives a list of batches from the `zksync_transaction_batches` table with the numbers defined in the input list. + + ## Parameters + - `numbers`: The list of batch numbers to retrieve from the database. + - `options`: Options passed to `Chain.select_repo()`. + + ## Returns + - A list of `Explorer.Chain.ZkSync.TransactionBatch` if at least one batch matches the numbers from the list. The output list could be less than the input list. + - An empty list (`[]`) if no batches with numbers from the list are found. + """ + @spec batches(maybe_improper_list(integer(), []), keyword()) :: [Explorer.Chain.ZkSync.TransactionBatch] + def batches(numbers, options) + when is_list(numbers) and + is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + base_query = from(tb in TransactionBatch, order_by: [desc: tb.number]) + + base_query + |> where([tb], tb.number in ^numbers) + |> Chain.join_associations(necessity_by_association) + |> select_repo(options).all() + end + + @doc """ + Receives a list of batches from the `zksync_transaction_batches` table. + + ## Parameters + - `options`: Options passed to `Chain.select_repo()`. (Optional) + + ## Returns + - If the option `confirmed?` is set, returns the ten latest committed batches (`Explorer.Chain.ZkSync.TransactionBatch`). + - Returns a list of `Explorer.Chain.ZkSync.TransactionBatch` based on the paging options if `confirmed?` is not set. + """ + @spec batches(keyword()) :: [Explorer.Chain.ZkSync.TransactionBatch] + @spec batches() :: [Explorer.Chain.ZkSync.TransactionBatch] + def batches(options \\ []) when is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + base_query = + from(tb in TransactionBatch, + order_by: [desc: tb.number] + ) + + query = + if Keyword.get(options, :confirmed?, false) do + base_query + |> Chain.join_associations(necessity_by_association) + |> where([tb], not is_nil(tb.commit_id) and tb.commit_id > 0) + |> limit(10) + else + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + base_query + |> Chain.join_associations(necessity_by_association) + |> page_batches(paging_options) + |> limit(^paging_options.page_size) + end + + select_repo(options).all(query) + end + + @doc """ + Receives a list of transactions from the `zksync_batch_l2_transactions` table included in a specific batch. + + ## Parameters + - `batch_number`: The number of batch which transactions were included to L1 as part of. + - `options`: Options passed to `Chain.select_repo()`. (Optional) + + ## Returns + - A list of `Explorer.Chain.ZkSync.BatchTransaction` belonging to the specified batch. + """ + @spec batch_transactions(non_neg_integer()) :: [Explorer.Chain.ZkSync.BatchTransaction] + @spec batch_transactions(non_neg_integer(), keyword()) :: [Explorer.Chain.ZkSync.BatchTransaction] + def batch_transactions(batch_number, options \\ []) + when is_integer(batch_number) or + is_binary(batch_number) do + query = from(batch in BatchTransaction, where: batch.batch_number == ^batch_number) + + select_repo(options).all(query) + end + + @doc """ + Gets the number of the earliest batch in the `zksync_transaction_batches` table where the commitment transaction is not set. + Batch #0 is filtered out, as it does not have a linked commitment transaction. + + ## Returns + - The number of a batch if it exists, otherwise `nil`. `nil` could mean either no batches imported yet or all imported batches are marked as committed or Batch #0 is the only available batch. + """ + @spec earliest_sealed_batch_number() :: non_neg_integer() | nil + def earliest_sealed_batch_number do + query = + from(tb in TransactionBatch, + select: tb.number, + where: is_nil(tb.commit_id) and tb.number > 0, + order_by: [asc: tb.number], + limit: 1 + ) + + query + |> Repo.one() + end + + @doc """ + Gets the number of the earliest batch in the `zksync_transaction_batches` table where the proving transaction is not set. + Batch #0 is filtered out, as it does not have a linked proving transaction. + + ## Returns + - The number of a batch if it exists, otherwise `nil`. `nil` could mean either no batches imported yet or all imported batches are marked as proven or Batch #0 is the only available batch. + """ + @spec earliest_unproven_batch_number() :: non_neg_integer() | nil + def earliest_unproven_batch_number do + query = + from(tb in TransactionBatch, + select: tb.number, + where: is_nil(tb.prove_id) and tb.number > 0, + order_by: [asc: tb.number], + limit: 1 + ) + + query + |> Repo.one() + end + + @doc """ + Gets the number of the earliest batch in the `zksync_transaction_batches` table where the executing transaction is not set. + Batch #0 is filtered out, as it does not have a linked executing transaction. + + ## Returns + - The number of a batch if it exists, otherwise `nil`. `nil` could mean either no batches imported yet or all imported batches are marked as executed or Batch #0 is the only available batch. + """ + @spec earliest_unexecuted_batch_number() :: non_neg_integer() | nil + def earliest_unexecuted_batch_number do + query = + from(tb in TransactionBatch, + select: tb.number, + where: is_nil(tb.execute_id) and tb.number > 0, + order_by: [asc: tb.number], + limit: 1 + ) + + query + |> Repo.one() + end + + @doc """ + Gets the number of the oldest batch from the `zksync_transaction_batches` table. + + ## Returns + - The number of a batch if it exists, otherwise `nil`. `nil` means that there is no batches imported yet. + """ + @spec oldest_available_batch_number() :: non_neg_integer() | nil + def oldest_available_batch_number do + query = + from(tb in TransactionBatch, + select: tb.number, + order_by: [asc: tb.number], + limit: 1 + ) + + query + |> Repo.one() + end + + @doc """ + Gets the number of the youngest (the most recent) imported batch from the `zksync_transaction_batches` table. + + ## Returns + - The number of a batch if it exists, otherwise `nil`. `nil` means that there is no batches imported yet. + """ + @spec latest_available_batch_number() :: non_neg_integer() | nil + def latest_available_batch_number do + query = + from(tb in TransactionBatch, + select: tb.number, + order_by: [desc: tb.number], + limit: 1 + ) + + query + |> Repo.one() + end + + @doc """ + Reads a list of L1 transactions by their hashes from the `zksync_lifecycle_l1_transactions` table. + + ## Parameters + - `l1_tx_hashes`: A list of hashes to retrieve L1 transactions for. + + ## Returns + - A list of `Explorer.Chain.ZkSync.LifecycleTransaction` corresponding to the hashes from the input list. The output list may be smaller than the input list. + """ + @spec lifecycle_transactions(maybe_improper_list(binary(), [])) :: [Explorer.Chain.ZkSync.LifecycleTransaction] + def lifecycle_transactions(l1_tx_hashes) do + query = + from( + lt in LifecycleTransaction, + select: {lt.hash, lt.id}, + where: lt.hash in ^l1_tx_hashes + ) + + Repo.all(query, timeout: :infinity) + end + + @doc """ + Determines the next index for the L1 transaction available in the `zksync_lifecycle_l1_transactions` table. + + ## Returns + - The next available index. If there are no L1 transactions imported yet, it will return `1`. + """ + @spec next_id() :: non_neg_integer() + def next_id do + query = + from(lt in LifecycleTransaction, + select: lt.id, + order_by: [desc: lt.id], + limit: 1 + ) + + last_id = + query + |> Repo.one() + |> Kernel.||(0) + + last_id + 1 + end + + defp page_batches(query, %PagingOptions{key: nil}), do: query + + defp page_batches(query, %PagingOptions{key: {number}}) do + from(tb in query, where: tb.number < ^number) + end +end diff --git a/apps/explorer/lib/explorer/chain/zksync/transaction_batch.ex b/apps/explorer/lib/explorer/chain/zksync/transaction_batch.ex new file mode 100644 index 000000000000..3f6ac409cee3 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/zksync/transaction_batch.ex @@ -0,0 +1,83 @@ +defmodule Explorer.Chain.ZkSync.TransactionBatch do + @moduledoc "Models a batch of transactions for ZkSync." + + use Explorer.Schema + + alias Explorer.Chain.{ + Block, + Hash, + Wei + } + + alias Explorer.Chain.ZkSync.{BatchTransaction, LifecycleTransaction} + + @optional_attrs ~w(commit_id prove_id execute_id)a + + @required_attrs ~w(number timestamp l1_tx_count l2_tx_count root_hash l1_gas_price l2_fair_gas_price start_block end_block)a + + @type t :: %__MODULE__{ + number: non_neg_integer(), + timestamp: DateTime.t(), + l1_tx_count: non_neg_integer(), + l2_tx_count: non_neg_integer(), + root_hash: Hash.t(), + l1_gas_price: Wei.t(), + l2_fair_gas_price: Wei.t(), + start_block: Block.block_number(), + end_block: Block.block_number(), + commit_id: non_neg_integer() | nil, + commit_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil, + prove_id: non_neg_integer() | nil, + prove_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil, + execute_id: non_neg_integer() | nil, + execute_transaction: %Ecto.Association.NotLoaded{} | LifecycleTransaction.t() | nil + } + + @primary_key {:number, :integer, autogenerate: false} + schema "zksync_transaction_batches" do + field(:timestamp, :utc_datetime_usec) + field(:l1_tx_count, :integer) + field(:l2_tx_count, :integer) + field(:root_hash, Hash.Full) + field(:l1_gas_price, Wei) + field(:l2_fair_gas_price, Wei) + field(:start_block, :integer) + field(:end_block, :integer) + + belongs_to(:commit_transaction, LifecycleTransaction, + foreign_key: :commit_id, + references: :id, + type: :integer + ) + + belongs_to(:prove_transaction, LifecycleTransaction, + foreign_key: :prove_id, + references: :id, + type: :integer + ) + + belongs_to(:execute_transaction, LifecycleTransaction, + foreign_key: :execute_id, + references: :id, + type: :integer + ) + + has_many(:l2_transactions, BatchTransaction, foreign_key: :batch_number) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = batches, attrs \\ %{}) do + batches + |> cast(attrs, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:commit_id) + |> foreign_key_constraint(:prove_id) + |> foreign_key_constraint(:execute_id) + |> unique_constraint(:number) + end +end diff --git a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex index cb8a2279b1de..764ca7c69a4e 100644 --- a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex +++ b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex @@ -9,6 +9,7 @@ defmodule Explorer.ChainSpec.GenesisData do alias Explorer.ChainSpec.Geth.Importer, as: GethImporter alias Explorer.ChainSpec.Parity.Importer + alias Explorer.Helper alias HTTPoison.Response @interval :timer.minutes(2) @@ -55,6 +56,10 @@ defmodule Explorer.ChainSpec.GenesisData do {:noreply, state} end + @doc """ + Fetches pre-mined balances and pre-compiled smart-contract bytecodes from genesis.json + """ + @spec fetch_genesis_data() :: Task.t() | :ok def fetch_genesis_data do path = Application.get_env(:explorer, __MODULE__)[:chain_spec_path] @@ -85,7 +90,7 @@ defmodule Explorer.ChainSpec.GenesisData do end defp fetch_spec(path) do - if valid_url?(path) do + if Helper.valid_url?(path) do fetch_from_url(path) else fetch_from_file(path) @@ -108,10 +113,4 @@ defmodule Explorer.ChainSpec.GenesisData do {:error, reason} end end - - defp valid_url?(string) do - uri = URI.parse(string) - - uri.scheme != nil && uri.host =~ "." - end end diff --git a/apps/explorer/lib/explorer/chain_spec/geth/importer.ex b/apps/explorer/lib/explorer/chain_spec/geth/importer.ex index 9c8083f6fc77..40db5445a83b 100644 --- a/apps/explorer/lib/explorer/chain_spec/geth/importer.ex +++ b/apps/explorer/lib/explorer/chain_spec/geth/importer.ex @@ -7,8 +7,8 @@ defmodule Explorer.ChainSpec.Geth.Importer do require Logger alias EthereumJSONRPC.Blocks - alias Explorer.Chain - alias Explorer.Chain.Hash.Address, as: AddressHash + alias Explorer.{Chain, Helper} + alias Explorer.Chain.Hash.Address def import_genesis_accounts(chain_spec) do balance_params = @@ -50,11 +50,25 @@ defmodule Explorer.ChainSpec.Geth.Importer do Chain.import(params) end + @spec genesis_accounts(any()) :: [%{address_hash: Address.t(), value: integer(), contract_code: String.t()}] def genesis_accounts(%{"genesis" => genesis}) do genesis_accounts(genesis) end - def genesis_accounts(chain_spec) do + def genesis_accounts(raw_accounts) when is_list(raw_accounts) do + raw_accounts + |> Enum.map(fn account -> + with {:ok, address_hash} <- Chain.string_to_address_hash(account["address"]), + balance <- Helper.parse_number(account["balance"]) do + %{address_hash: address_hash, value: balance, contract_code: account["bytecode"]} + else + _ -> nil + end + end) + |> Enum.filter(&(!is_nil(&1))) + end + + def genesis_accounts(chain_spec) when is_map(chain_spec) do accounts = chain_spec["alloc"] if accounts do @@ -73,8 +87,8 @@ defmodule Explorer.ChainSpec.Geth.Importer do end) |> Stream.map(fn {address, %{"balance" => value} = params} -> formatted_address = if String.starts_with?(address, "0x"), do: address, else: "0x" <> address - {:ok, address_hash} = AddressHash.cast(formatted_address) - balance = parse_number(value) + {:ok, address_hash} = Address.cast(formatted_address) + balance = Helper.parse_number(value) code = params["code"] @@ -82,18 +96,4 @@ defmodule Explorer.ChainSpec.Geth.Importer do end) |> Enum.to_list() end - - defp parse_number("0x" <> hex_number) do - {number, ""} = Integer.parse(hex_number, 16) - - number - end - - defp parse_number(""), do: 0 - - defp parse_number(string_number) do - {number, ""} = Integer.parse(string_number, 10) - - number - end end diff --git a/apps/explorer/lib/explorer/chain_spec/parity/importer.ex b/apps/explorer/lib/explorer/chain_spec/parity/importer.ex index 9fd32144f4eb..58dde7fcaa7c 100644 --- a/apps/explorer/lib/explorer/chain_spec/parity/importer.ex +++ b/apps/explorer/lib/explorer/chain_spec/parity/importer.ex @@ -126,9 +126,9 @@ defmodule Explorer.ChainSpec.Parity.Importer do |> Stream.map(fn {address, %{"balance" => value} = params} -> formatted_address = if String.starts_with?(address, "0x"), do: address, else: "0x" <> address {:ok, address_hash} = AddressHash.cast(formatted_address) - balance = parse_number(value) + balance = ExplorerHelper.parse_number(value) - nonce = parse_number(params["nonce"] || "0") + nonce = ExplorerHelper.parse_number(params["nonce"] || "0") code = params["constructor"] %{address_hash: address_hash, value: balance, nonce: nonce, contract_code: code} @@ -162,26 +162,16 @@ defmodule Explorer.ChainSpec.Parity.Importer do defp parse_hex_numbers(rewards) when is_map(rewards) do Enum.map(rewards, fn {hex_block_number, hex_reward} -> - block_number = parse_number(hex_block_number) - {:ok, reward} = hex_reward |> parse_number() |> Wei.cast() + block_number = ExplorerHelper.parse_number(hex_block_number) + {:ok, reward} = hex_reward |> ExplorerHelper.parse_number() |> Wei.cast() {block_number, reward} end) end defp parse_hex_numbers(reward) do - {:ok, reward} = reward |> parse_number() |> Wei.cast() + {:ok, reward} = reward |> ExplorerHelper.parse_number() |> Wei.cast() [{0, reward}] end - - defp parse_number("0x" <> hex_number) do - {number, ""} = Integer.parse(hex_number, 16) - - number - end - - defp parse_number(string_number) do - ExplorerHelper.parse_integer(string_number) - end end diff --git a/apps/explorer/lib/explorer/counters/average_block_time.ex b/apps/explorer/lib/explorer/counters/average_block_time.ex index 9075415854a0..02e8e3464705 100644 --- a/apps/explorer/lib/explorer/counters/average_block_time.ex +++ b/apps/explorer/lib/explorer/counters/average_block_time.ex @@ -1,9 +1,8 @@ defmodule Explorer.Counters.AverageBlockTime do - use GenServer - @moduledoc """ - Caches the number of token holders of a token. + Caches the average block time in milliseconds. """ + use GenServer import Ecto.Query, only: [from: 2, where: 2] @@ -11,6 +10,9 @@ defmodule Explorer.Counters.AverageBlockTime do alias Explorer.Repo alias Timex.Duration + @num_of_blocks 100 + @offset 100 + @doc """ Starts a process to periodically update the counter of the token holders. """ @@ -62,10 +64,13 @@ defmodule Explorer.Counters.AverageBlockTime do end defp refresh_timestamps do + first_block_from_config = Application.get_env(:indexer, :first_block) + base_query = from(block in Block, - limit: 100, - offset: 100, + where: block.number > ^first_block_from_config, + limit: ^@num_of_blocks, + offset: ^@offset, order_by: [desc: block.number], select: {block.number, block.timestamp} ) @@ -78,13 +83,13 @@ defmodule Explorer.Counters.AverageBlockTime do |> where(consensus: true) end - timestamps_row = + raw_timestamps = timestamps_query |> Repo.all() timestamps = - timestamps_row - |> Enum.sort_by(fn {_, timestamp} -> timestamp end, &>=/2) + raw_timestamps + |> Enum.sort_by(fn {_, timestamp} -> timestamp end, &Timex.after?/2) |> Enum.map(fn {number, timestamp} -> {number, DateTime.to_unix(timestamp, :millisecond)} end) @@ -125,7 +130,7 @@ defmodule Explorer.Counters.AverageBlockTime do defp compose_durations(durations, block_number, last_block_number, last_timestamp, timestamp) do block_numbers_range = last_block_number - block_number - if block_numbers_range == 0 do + if block_numbers_range <= 0 do {durations, block_number, timestamp} else duration = (last_timestamp - timestamp) / block_numbers_range diff --git a/apps/explorer/lib/explorer/counters/block_burned_fee_counter.ex b/apps/explorer/lib/explorer/counters/block_burnt_fee_counter.ex similarity index 89% rename from apps/explorer/lib/explorer/counters/block_burned_fee_counter.ex rename to apps/explorer/lib/explorer/counters/block_burnt_fee_counter.ex index 85e9359f4294..9c87e313ffe7 100644 --- a/apps/explorer/lib/explorer/counters/block_burned_fee_counter.ex +++ b/apps/explorer/lib/explorer/counters/block_burnt_fee_counter.ex @@ -1,15 +1,15 @@ -defmodule Explorer.Counters.BlockBurnedFeeCounter do +defmodule Explorer.Counters.BlockBurntFeeCounter do @moduledoc """ - Caches Block Burned Fee counter. + Caches Block Burnt Fee counter. """ use GenServer alias Explorer.Chain alias Explorer.Counters.Helper - @cache_name :block_burned_fee_counter + @cache_name :block_burnt_fee_counter - config = Application.compile_env(:explorer, Explorer.Counters.BlockBurnedFeeCounter) + config = Application.compile_env(:explorer, __MODULE__) @enable_consolidation Keyword.get(config, :enable_consolidation) @spec start_link(term()) :: GenServer.on_start() diff --git a/apps/explorer/lib/explorer/counters/block_priority_fee_counter.ex b/apps/explorer/lib/explorer/counters/block_priority_fee_counter.ex index f51deb4e2e7a..152679fe830d 100644 --- a/apps/explorer/lib/explorer/counters/block_priority_fee_counter.ex +++ b/apps/explorer/lib/explorer/counters/block_priority_fee_counter.ex @@ -1,6 +1,6 @@ defmodule Explorer.Counters.BlockPriorityFeeCounter do @moduledoc """ - Caches Block Burned Fee counter. + Caches Block Priority Fee counter. """ use GenServer diff --git a/apps/explorer/lib/explorer/counters/fresh_pending_transactions_counter.ex b/apps/explorer/lib/explorer/counters/fresh_pending_transactions_counter.ex new file mode 100644 index 000000000000..3a4b1548ff53 --- /dev/null +++ b/apps/explorer/lib/explorer/counters/fresh_pending_transactions_counter.ex @@ -0,0 +1,95 @@ +defmodule Explorer.Counters.FreshPendingTransactionsCounter do + @moduledoc """ + Caches number of pending transactions for last 30 minutes. + + It loads the sum asynchronously and in a time interval of :cache_period (default to 5 minutes). + """ + + use GenServer + + import Ecto.Query + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Transaction + + @counter_type "pending_transaction_count_30min" + + @doc """ + Starts a process to periodically update the counter. + """ + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_args) do + {:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}} + end + + defp schedule_next_consolidation do + Process.send_after(self(), :consolidate, cache_interval()) + end + + @impl true + def handle_continue(:ok, %{consolidate?: true} = state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @impl true + def handle_continue(:ok, state) do + {:noreply, state} + end + + @impl true + def handle_info(:consolidate, state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @doc """ + Fetches the value for a `#{@counter_type}` counter type from the `last_fetched_counters` table. + """ + def fetch(options) do + Chain.get_last_fetched_counter(@counter_type, options) + end + + @doc """ + Consolidates the info by populating the `last_fetched_counters` table with the current database information. + """ + def consolidate do + query = + from(transaction in Transaction, + where: is_nil(transaction.block_hash) and transaction.inserted_at >= ago(30, "minute"), + select: count(transaction.hash) + ) + + count = Repo.one!(query, timeout: :infinity) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @counter_type, + value: count + }) + end + + @doc """ + Returns a boolean that indicates whether consolidation is enabled + + In order to choose whether or not to enable the scheduler and the initial + consolidation, change the following Explorer config: + + `config :explorer, #{__MODULE__}, enable_consolidation: true` + + to: + + `config :explorer, #{__MODULE__}, enable_consolidation: false` + """ + def enable_consolidation?, do: Application.get_env(:explorer, __MODULE__)[:enable_consolidation] + + defp cache_interval, do: Application.get_env(:explorer, __MODULE__)[:cache_period] +end diff --git a/apps/explorer/lib/explorer/counters/last_fetched_counter.ex b/apps/explorer/lib/explorer/counters/last_fetched_counter.ex index e4d684c2f167..0acc752e9538 100644 --- a/apps/explorer/lib/explorer/counters/last_fetched_counter.ex +++ b/apps/explorer/lib/explorer/counters/last_fetched_counter.ex @@ -3,19 +3,13 @@ defmodule Explorer.Counters.LastFetchedCounter do Stores last fetched counters. """ - alias Explorer.Counters.LastFetchedCounter use Explorer.Schema import Ecto.Changeset - @type t :: %LastFetchedCounter{ - counter_type: String.t(), - value: Decimal.t() - } - @primary_key false - schema "last_fetched_counters" do - field(:counter_type, :string) + typed_schema "last_fetched_counters" do + field(:counter_type, :string, null: false) field(:value, :decimal) timestamps() diff --git a/apps/explorer/lib/explorer/counters/last_output_root_size_counter.ex b/apps/explorer/lib/explorer/counters/last_output_root_size_counter.ex new file mode 100644 index 000000000000..c910dbe6dae7 --- /dev/null +++ b/apps/explorer/lib/explorer/counters/last_output_root_size_counter.ex @@ -0,0 +1,112 @@ +defmodule Explorer.Counters.LastOutputRootSizeCounter do + @moduledoc """ + Caches number of transactions in last output root. + + It loads the count asynchronously and in a time interval of :cache_period (default to 5 minutes). + """ + + use GenServer + + import Ecto.Query + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Optimism.OutputRoot + alias Explorer.Chain.Transaction + + @counter_type "last_output_root_size_count" + + @doc """ + Starts a process to periodically update the counter. + """ + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_args) do + {:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}} + end + + defp schedule_next_consolidation do + Process.send_after(self(), :consolidate, cache_interval()) + end + + @impl true + def handle_continue(:ok, %{consolidate?: true} = state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @impl true + def handle_continue(:ok, state) do + {:noreply, state} + end + + @impl true + def handle_info(:consolidate, state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @doc """ + Fetches the value for a `#{@counter_type}` counter type from the `last_fetched_counters` table. + """ + def fetch(options) do + Chain.get_last_fetched_counter(@counter_type, options |> Keyword.put_new(:nullable, true)) + end + + @doc """ + Consolidates the info by populating the `last_fetched_counters` table with the current database information. + """ + def consolidate do + output_root_query = + from(root in OutputRoot, + select: {root.l2_block_number}, + order_by: [desc: root.l2_output_index], + limit: 2 + ) + + count = + case output_root_query |> Repo.all() do + [{last_block_number}, {prev_block_number}] -> + query = + from(transaction in Transaction, + where: + not is_nil(transaction.block_hash) and transaction.block_number > ^prev_block_number and + transaction.block_number <= ^last_block_number, + select: count(transaction.hash) + ) + + Repo.one!(query, timeout: :infinity) + + _ -> + nil + end + + Chain.upsert_last_fetched_counter(%{ + counter_type: @counter_type, + value: count + }) + end + + @doc """ + Returns a boolean that indicates whether consolidation is enabled + + In order to choose whether or not to enable the scheduler and the initial + consolidation, change the following Explorer config: + + `config :explorer, #{__MODULE__}, enable_consolidation: true` + + to: + + `config :explorer, #{__MODULE__}, enable_consolidation: false` + """ + def enable_consolidation?, do: Application.get_env(:explorer, __MODULE__)[:enable_consolidation] + + defp cache_interval, do: Application.get_env(:explorer, __MODULE__)[:cache_period] +end diff --git a/apps/explorer/lib/explorer/counters/transactions_24h_stats.ex b/apps/explorer/lib/explorer/counters/transactions_24h_stats.ex new file mode 100644 index 000000000000..80bef49ec046 --- /dev/null +++ b/apps/explorer/lib/explorer/counters/transactions_24h_stats.ex @@ -0,0 +1,143 @@ +defmodule Explorer.Counters.Transactions24hStats do + @moduledoc """ + Caches number of transactions for last 24 hours, sum of transaction fees for last 24 hours and average transaction fee for last 24 hours counters. + + It loads the counters asynchronously and in a time interval of :cache_period (default to 1 hour). + """ + + use GenServer + + import Ecto.Query + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Transaction + + @tx_count_name "transaction_count_24h" + @tx_fee_sum_name "transaction_fee_sum_24h" + @tx_fee_average_name "transaction_fee_average_24h" + + @doc """ + Starts a process to periodically update the counters. + """ + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_args) do + {:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}} + end + + defp schedule_next_consolidation do + Process.send_after(self(), :consolidate, cache_interval()) + end + + @impl true + def handle_continue(:ok, %{consolidate?: true} = state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @impl true + def handle_continue(:ok, state) do + {:noreply, state} + end + + @impl true + def handle_info(:consolidate, state) do + consolidate() + schedule_next_consolidation() + + {:noreply, state} + end + + @doc """ + Fetches the value for a `#{@tx_count_name}` counter type from the `last_fetched_counters` table. + """ + def fetch_count(options) do + Chain.get_last_fetched_counter(@tx_count_name, options) + end + + @doc """ + Fetches the value for a `#{@tx_fee_sum_name}` counter type from the `last_fetched_counters` table. + """ + def fetch_fee_sum(options) do + Chain.get_last_fetched_counter(@tx_fee_sum_name, options) + end + + @doc """ + Fetches the value for a `#{@tx_fee_average_name}` counter type from the `last_fetched_counters` table. + """ + def fetch_fee_average(options) do + Chain.get_last_fetched_counter(@tx_fee_average_name, options) + end + + @doc """ + Consolidates the info by populating the `last_fetched_counters` table with the current database information. + """ + def consolidate do + fee_query = + dynamic( + [transaction, block], + fragment( + "COALESCE(?, ? + LEAST(?, ?))", + transaction.gas_price, + block.base_fee_per_gas, + transaction.max_priority_fee_per_gas, + transaction.max_fee_per_gas - block.base_fee_per_gas + ) * transaction.gas_used + ) + + sum_query = dynamic([_, _], sum(^fee_query)) + avg_query = dynamic([_, _], avg(^fee_query)) + + query = + from(transaction in Transaction, + join: block in assoc(transaction, :block), + where: block.timestamp >= ago(24, "hour"), + select: %{count: count(transaction.hash)}, + select_merge: ^%{fee_sum: sum_query}, + select_merge: ^%{fee_average: avg_query} + ) + + %{ + count: count, + fee_sum: fee_sum, + fee_average: fee_average + } = Repo.one!(query, timeout: :infinity) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @tx_count_name, + value: count + }) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @tx_fee_sum_name, + value: fee_sum + }) + + Chain.upsert_last_fetched_counter(%{ + counter_type: @tx_fee_average_name, + value: fee_average + }) + end + + @doc """ + Returns a boolean that indicates whether consolidation is enabled + + In order to choose whether or not to enable the scheduler and the initial + consolidation, change the following Explorer config: + + `config :explorer, #{__MODULE__}, enable_consolidation: true` + + to: + + `config :explorer, #{__MODULE__}, enable_consolidation: false` + """ + def enable_consolidation?, do: Application.get_env(:explorer, __MODULE__)[:enable_consolidation] + + defp cache_interval, do: Application.get_env(:explorer, __MODULE__)[:cache_period] +end diff --git a/apps/explorer/lib/explorer/encrypted/address_hash.ex b/apps/explorer/lib/explorer/encrypted/address_hash.ex index 4518951298ee..fb90251f1e60 100644 --- a/apps/explorer/lib/explorer/encrypted/address_hash.ex +++ b/apps/explorer/lib/explorer/encrypted/address_hash.ex @@ -2,4 +2,6 @@ defmodule Explorer.Encrypted.AddressHash do @moduledoc false use Explorer.Encrypted.Types.AddressHash, vault: Explorer.Vault + + @type t :: Explorer.Chain.Hash.Address.t() end diff --git a/apps/explorer/lib/explorer/encrypted/binary.ex b/apps/explorer/lib/explorer/encrypted/binary.ex index 6de296ded69d..7a5e62bd39af 100644 --- a/apps/explorer/lib/explorer/encrypted/binary.ex +++ b/apps/explorer/lib/explorer/encrypted/binary.ex @@ -2,4 +2,6 @@ defmodule Explorer.Encrypted.Binary do @moduledoc false use Cloak.Ecto.Binary, vault: Explorer.Vault + + @type t :: binary() end diff --git a/apps/explorer/lib/explorer/encrypted/transaction_hash.ex b/apps/explorer/lib/explorer/encrypted/transaction_hash.ex index a783cb899b2b..fa240c27e3b1 100644 --- a/apps/explorer/lib/explorer/encrypted/transaction_hash.ex +++ b/apps/explorer/lib/explorer/encrypted/transaction_hash.ex @@ -2,4 +2,6 @@ defmodule Explorer.Encrypted.TransactionHash do @moduledoc false use Explorer.Encrypted.Types.TransactionHash, vault: Explorer.Vault + + @type t :: Explorer.Chain.Hash.Full.t() end diff --git a/apps/explorer/lib/explorer/eth_rpc.ex b/apps/explorer/lib/explorer/eth_rpc.ex index 889fdabbba6e..0487a60c619f 100644 --- a/apps/explorer/lib/explorer/eth_rpc.ex +++ b/apps/explorer/lib/explorer/eth_rpc.ex @@ -2,13 +2,27 @@ defmodule Explorer.EthRPC do @moduledoc """ Ethereum JSON RPC methods logic implementation. """ + import Explorer.EthRpcHelper alias Ecto.Type, as: EctoType - alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, Data, Hash, Hash.Address, Wei} - alias Explorer.Chain.Cache.BlockNumber + alias Explorer.{BloomFilter, Chain, Helper, Repo} + + alias Explorer.Chain.{ + Block, + Data, + DenormalizationHelper, + Hash, + Hash.Address, + Transaction, + Transaction.Status, + Wei + } + + alias Explorer.Chain.Cache.{BlockNumber, GasPriceOracle} alias Explorer.Etherscan.{Blocks, Logs, RPC} + @nil_gas_price_message "Gas price is not estimated yet" + @methods %{ "eth_blockNumber" => %{ action: :eth_block_number, @@ -54,13 +68,13 @@ defmodule Explorer.EthRPC do action: :eth_get_logs, notes: """ Will never return more than 1000 log entries.\n - For this reason, you can use pagination options to request the next page. Pagination options params: {"logIndex": "3D", "blockNumber": "6423AC", "transactionIndex": 53} which include parameters from the last log received from the previous request. These three parameters are required for pagination. + For this reason, you can use pagination options to request the next page. Pagination options params: {"logIndex": "3D", "blockNumber": "6423AC"} which include parameters from the last log received from the previous request. These three parameters are required for pagination. """, example: """ {"id": 0, "jsonrpc": "2.0", "method": "eth_getLogs", "params": [ {"address": "0xc78Be425090Dbd437532594D12267C5934Cc6c6f", - "paging_options": {"logIndex": "3D", "blockNumber": "6423AC", "transactionIndex": 53}, + "paging_options": {"logIndex": "3D", "blockNumber": "6423AC"}, "fromBlock": "earliest", "toBlock": "latest", "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}]} @@ -84,6 +98,553 @@ defmodule Explorer.EthRPC do }] } """ + }, + "eth_gasPrice" => %{ + action: :eth_gas_price, + notes: """ + Returns the average gas price per gas in wei. + """, + example: """ + {"jsonrpc": "2.0", "id": 4, "method": "eth_gasPrice", "params": []} + """, + params: [], + result: """ + {"jsonrpc": "2.0", "id": 4, "result": "0xbf69c09bb"} + """ + }, + "eth_getTransactionByHash" => %{ + action: :eth_get_transaction_by_hash, + notes: """ + """, + example: """ + {"jsonrpc": "2.0", "id": 4, "method": "eth_getTransactionByHash", "params": ["0x98318a5a22e363928d4565382c1022a8aed169b6a657f639c2f5c6e2c5114e4c"]} + """, + params: [ + %{ + name: "Data", + description: "32 Bytes - transaction hash to get", + type: "string", + default: nil, + required: true + } + ], + result: """ + { + "jsonrpc": "2.0", + "result": { + "blockHash": "0x33c4ddb4478395b9d73aad2eb8640004a4a312da29ebccbaa33933a43edda019", + "blockNumber": "0x87855e", + "chainId": "0x5", + "from": "0xe38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d", + "gas": "0x186a0", + "gasPrice": "0x195d", + "hash": "0xfe524295c6c01ab25645035a228387bf0e64c8af429f3dd9d6ef2e3b05337839", + "input": "0xe9e05c42000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d0000000000000000000000000000000000000000000000000001c6bf5263400000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "maxFeePerGas": null, + "maxPriorityFeePerGas": null, + "nonce": "0x1", + "r": "0xf2a3f18fd456ef9a9d6201cf622b5ad14db9cfc6786ba574e036037f80a15d61", + "s": "0x4cbb018dc0a966cd15a6bf5f3d432c72127639314d6aeb7a6bbb36000d86dc08", + "to": "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa", + "transactionIndex": "0x7f", + "type": "0x0", + "v": "0x2d", + "value": "0x1c6bf52634000" + }, + "id": 4 + } + """ + }, + "eth_getTransactionReceipt" => %{ + action: :eth_get_transaction_receipt, + notes: """ + """, + example: """ + {"jsonrpc": "2.0","id": 0,"method": "eth_getTransactionReceipt","params": ["0xFE524295C6C01AB25645035A228387BF0E64C8AF429F3DD9D6EF2E3B05337839"]} + """, + params: [ + %{ + name: "Data", + description: "32 Bytes - transaction hash to get", + type: "string", + default: nil, + required: true + } + ], + result: """ + { + "jsonrpc": "2.0", + "result": { + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000040000000000000000000000000002000000000000000000000000000000000000000000000000030000000000000000000800000000000000000000000000000000000000000000000002000000008000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000002000000000000000080000000000000000000000", + "blockHash": "0x33c4ddb4478395b9d73aad2eb8640004a4a312da29ebccbaa33933a43edda019", + "blockNumber": "0x87855e", + "contractAddress": null, + "cumulativeGasUsed": "0x15a9b84", + "effectiveGasPrice": "0x195d", + "from": "0xe38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d", + "gasUsed": "0x9821", + "logs": [ + { + "address": "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa", + "blockHash": "0x33c4ddb4478395b9d73aad2eb8640004a4a312da29ebccbaa33933a43edda019", + "blockNumber": "0x87855e", + "data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000490000000000000000000000000000000000000000000000000001c6bf526340000000000000000000000000000000000000000000000000000001c6bf5263400000000000000186a0000000000000000000000000000000000000000000000000", + "logIndex": "0xdf", + "removed": false, + "topics": [ + "0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32", + "0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d", + "0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "transactionHash": "0xfe524295c6c01ab25645035a228387bf0e64c8af429f3dd9d6ef2e3b05337839", + "transactionIndex": "0x7f" + } + ], + "status": "0x1", + "to": "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa", + "transactionHash": "0xfe524295c6c01ab25645035a228387bf0e64c8af429f3dd9d6ef2e3b05337839", + "transactionIndex": "0x7f", + "type": "0x0" + }, + "id": 0 + } + """ + }, + "eth_chainId" => %{ + action: :eth_chain_id, + notes: """ + """, + example: """ + {"jsonrpc": "2.0","id": 0,"method": "eth_chainId","params": []} + """, + params: [], + result: """ + { + "jsonrpc": "2.0", + "id": 0, + "result": "0x5" + } + """ + }, + "eth_maxPriorityFeePerGas" => %{ + action: :eth_max_priority_fee_per_gas, + notes: """ + """, + example: """ + {"jsonrpc": "2.0","id": 0,"method": "eth_maxPriorityFeePerGas","params": []} + """, + params: [], + result: """ + { + "jsonrpc": "2.0", + "id": 0, + "result": "0x3b9aca00" + } + """ + } + } + + @proxy_methods %{ + "eth_getTransactionCount" => %{ + arity: 2, + params_validators: [&address_hash_validator/1, &block_validator/1], + example: """ + {"id": 0, "jsonrpc": "2.0", "method": "eth_getTransactionCount", "params": ["0x0000000000000000000000000000000000000007", "latest"]} + """, + result: """ + {"id": 0, "jsonrpc": "2.0", "result": "0x2"} + """ + }, + "eth_getCode" => %{ + arity: 2, + params_validators: [&address_hash_validator/1, &block_validator/1], + example: """ + {"jsonrpc":"2.0","id": 0,"method":"eth_getCode","params":["0x1BF313AADe1e1f76295943f40B558Eb13Db7aA99", "latest"]} + """, + result: """ + { + "jsonrpc": "2.0", + "result": "0x60806040523661001357610011610017565b005b6100115b610027610022610067565b61009f565b565b606061004e838360405180606001604052806027815260200161026b602791396100c3565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b3660008037600080366000845af43d6000803e8080156100be573d6000f35b3d6000fd5b6060600080856001600160a01b0316856040516100e0919061021b565b600060405180830381855af49150503d806000811461011b576040519150601f19603f3d011682016040523d82523d6000602084013e610120565b606091505b50915091506101318683838761013b565b9695505050505050565b606083156101af5782516000036101a8576001600160a01b0385163b6101a85760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b50816101b9565b6101b983836101c1565b949350505050565b8151156101d15781518083602001fd5b8060405162461bcd60e51b815260040161019f9190610237565b60005b838110156102065781810151838201526020016101ee565b83811115610215576000848401525b50505050565b6000825161022d8184602087016101eb565b9190910192915050565b60208152600082518060208401526102568160408501602087016101eb565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220ef6e0977d993c1b69ec75a2f9fd6a53122d4ad4f9d71477641195afb6a6a45dd64736f6c634300080f0033", + "id": 0 + } + """ + }, + "eth_getStorageAt" => %{ + arity: 3, + params_validators: [&address_hash_validator/1, &integer_validator/1, &block_validator/1], + example: """ + {"jsonrpc":"2.0","id":4,"method":"eth_getStorageAt","params":["0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F", "0x", "latest"]} + """, + result: """ + { + "jsonrpc": "2.0", + "result": "0x0000000000000000000000000000000000000000000000000000000000000000", + "id": 4 + } + """ + }, + "eth_estimateGas" => %{ + arity: 2, + params_validators: [ð_call_validator/1, &block_validator/1], + example: """ + {"jsonrpc":"2.0","id": 0,"method":"eth_estimateGas","params":[{"to": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F", "input": "0xd4aae0c4", "from": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F"}, "latest"]} + """, + result: """ + { + "jsonrpc": "2.0", + "result": "0x5bb6", + "id": 0 + } + """ + }, + "eth_getBlockByNumber" => %{ + arity: 2, + params_validators: [&block_validator/1, &bool_validator/1], + example: """ + {"jsonrpc":"2.0","id": 0,"method":"eth_getBlockByNumber","params":["latest", false]} + """, + result: """ + { + "jsonrpc": "2.0", + "result": { + "baseFeePerGas": "0x7", + "blobGasUsed": "0x0", + "difficulty": "0x0", + "excessBlobGas": "0x4bc0000", + "extraData": "0xd883010d0a846765746888676f312e32312e35856c696e7578", + "gasLimit": "0x1c9c380", + "gasUsed": "0x29b80d", + "hash": "0xbc2e3a9caf7364d306fe4af34d2e9f0a3d478ed1a8e135bf7cd0845646c858f5", + "logsBloom": "0x022100021800180480000040e0008004001044020100000204080000a20001100100100002000802c00020194040204000020010000200400000020004212000804100a4242020041800108d0228082402000040090000c80001040080000080000600000224a0b00000d88000004803000000220008014000204010100040008000804408000004200000250010400004001481a80001080080404104114040032000307022969010004000840040000322400002010108490180088040205030055002481208004903100400070000104000002008001008080010002020001818002020a04000501101080000000000201004000001400040880000000000", + "miner": "0x94750381be1aba0504c666ee1db118f68f0780d4", + "mixHash": "0xd6b01921b81abdec5eccc9f5e17246be9dfec6d3bdbf59503bdeee2db3f97a57", + "nonce": "0x0000000000000000", + "number": "0xa0ff94", + "parentBeaconBlockRoot": "0xbd4670ba8503146561cb96962185fc251e2040eed07fccc749a26b8edbfd2d1c", + "parentHash": "0x7d4de3172a22e4549b28492ab9ffe6b5bf050b82d2c9b744133657aa7ae4385d", + "receiptsRoot": "0x13ae8ce96a643074f94bc1358b1ac1a3e3660856df943b9c6b60d499386e580d", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x2d07", + "stateRoot": "0x68510947af6edb94d0d1852d881589001318872b5bad832006c569e1a4f26871", + "timestamp": "0x65d07350", + "totalDifficulty": "0xa4a470", + "transactions": [ + "0xa3bb1b7bb5ee2d04114d47bbca1d8597c390e7c8ccfe04b5bfe96f6dfe897ec7", + "0xb6f680d4ba7e258e5e306744e61be1abb9b6cd005eb9423badc0b3603eb4ad5c", + "0x3c97b4ee54827e95ebf915dafdba9059ba5f4013c0371d443fe934a644725c60", + "0xd91e6db89992030da48d92825220053b1ea39f6d8d619c0f3fbc9a9e059c903e", + "0xbf763dc0a81dd2ef44f19673f001de560bce4db1499b7c0461c208afd863a62c", + "0xadd61d6e79560df74dc72891b2b19c83586d7857e313c0fdea9edbe1bfb11866", + "0xc66df09eaefac0348f48ce9e3f79e27a537bc8f274c525dd884f285d5e05bf31", + "0x217a26e8e407638e68364c2edebdae35f2a55eae080caa9ab31be430247a06e7", + "0xcae598dde02f35993cc4dad6f431596d8326a69b8f6563156edc3e970d6736d6", + "0xcfa79201e7574bce217f3f790f99bee8e0af45cffcd75ad17a9742630664df3f", + "0xe1e9b3b32e1098b3e08786407043410a6142481c1076818341cb05d7ebb3aaa3", + "0x7ce2eb696fd7c60e443bfeeeb39c2011d968b7bcfa40c20613549963f11e30a6", + "0xb4b5db2b4397e89b068ca01fc1b6bf8494a7fcd60e39e7059baef2968e874ba4", + "0x877e4ce429f4b64a095e0648b5ee69c31591116a697d03fddc5ff069302c944d", + "0xa5b8f358a3210221551250369c8dc2584c79fb424af1dd134bdab3a125eb1ea8", + "0x1c1d3df874c3ff9b84195bfe0bd5dbd50677443ff9a429bd01de4a18ccaf9293", + "0x79be4e1a433f250a35b7898916a0611f957fb7ca522836354eebfa421b2c8c99", + "0xd1c7fc2537a6627d0056e70a23bf90f988eabc518a31cd3d7520ec4ca0f9f9f0", + "0xb8c0b577257a0a184bf53454b68ce612a7567bcce48a64ee10e8b3d899c6ee16", + "0x26e81d1ba0109e5f50da13cb03d70a4fd5ffc97a0dad8e0c33fa7a8856db1480", + "0x4667088b1ab61818ebd08810a354dbe2f1ce9a4cb3f735aa692efcc8f15c7e5f" + ], + "transactionsRoot": "0x9d4d5a21e9ae6294a2a197c6d051a184c109882a7f74b7e63aaf3e64e4a77a33", + "uncles": [], + "withdrawals": [ + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x13d378", + "index": "0x1cdf824", + "validatorIndex": "0xa6d24" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x124012", + "index": "0x1cdf825", + "validatorIndex": "0xa6d25" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x175e6f", + "index": "0x1cdf826", + "validatorIndex": "0xa6d26" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x16b5fe", + "index": "0x1cdf827", + "validatorIndex": "0xa6d27" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x1660d2", + "index": "0x1cdf828", + "validatorIndex": "0xa6d28" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x145405", + "index": "0x1cdf829", + "validatorIndex": "0xa6d29" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x16246d", + "index": "0x1cdf82a", + "validatorIndex": "0xa6d2a" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x14a5a1", + "index": "0x1cdf82b", + "validatorIndex": "0xa6d2b" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x142199", + "index": "0x1cdf82c", + "validatorIndex": "0xa6d2c" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x182250", + "index": "0x1cdf82d", + "validatorIndex": "0xa6d2d" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x18b97e", + "index": "0x1cdf82e", + "validatorIndex": "0xa6d2e" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x151536", + "index": "0x1cdf82f", + "validatorIndex": "0xa6d2f" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x14bc4a", + "index": "0x1cdf830", + "validatorIndex": "0xa6d30" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x162f06", + "index": "0x1cdf831", + "validatorIndex": "0xa6d31" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x13563b", + "index": "0x1cdf832", + "validatorIndex": "0xa6d32" + }, + { + "address": "0x46e77b9485b13b4d401dac9ad3f59700a5200aeb", + "amount": "0x148d8b", + "index": "0x1cdf833", + "validatorIndex": "0xa6d33" + } + ], + "withdrawalsRoot": "0xc6a4b2cace2cc78c3a304731165b848e455fc7a3bf876837048cb4974a62c25f" + }, + "id": 0 + } + """ + }, + "eth_getBlockByHash" => %{ + arity: 2, + params_validators: [&hash_validator/1, &bool_validator/1], + example: """ + {"jsonrpc":"2.0","id": 0,"method":"eth_getBlockByHash","params":["0x2980314632a35ff83ef1f26a2a972259dca49353ed9368a04f21bcd7a5512231", false]} + """, + result: """ + { + "jsonrpc": "2.0", + "id": 0, + "result": { + "baseFeePerGas": "0x7", + "blobGasUsed": "0xc0000", + "difficulty": "0x0", + "excessBlobGas": "0x4b40000", + "extraData": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465", + "gasLimit": "0x1c9c380", + "gasUsed": "0x2ff140", + "hash": "0x2980314632a35ff83ef1f26a2a972259dca49353ed9368a04f21bcd7a5512231", + "logsBloom": "0x40200000202018800808200040082040800001000040000000200984800600000200000000000810000020014000200028000000200000010530034004202010000440800d00a0000100000800000820020100009040808c80004000000040000017000003040610800002002800081a405800080060140080004a100000000308220100000400020002000100004000040412020010020000018040000000010700804008040088108001020004110008026280800021824180002c00008200a01440120000223009022014801001120080000020080000090100020000281004102000802802a1820024000c00020008000290151802004000080000000804", + "miner": "0xb64a30399f7f6b0c154c2e7af0a3ec7b0a5b131a", + "mixHash": "0xe5cf393a9e4b40800fd4e4a1d2be0de08e7aabc83de5fd16ff719680d7a04253", + "nonce": "0x0000000000000000", + "number": "0xa21bc8", + "parentBeaconBlockRoot": "0xca280fd409ee503ae331931d64ee7fc29da9ed566cba6dfc4212a2f2f8004c41", + "parentHash": "0xc2fc3c51d15a2fe6f219079694865ffd9f8fe56e714d9bc49e9451e1c430acf9", + "receiptsRoot": "0x7941071eea76cec0eb4541854bd7820ef36c4d44ae51413063b45d9ea127313d", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x34cf", + "stateRoot": "0xcff5056271c6e6f6bf04d2e82392fa3ebcf2bc4aca9fe8801edcfcc261ddb557", + "timestamp": "0x65e324a4", + "totalDifficulty": "0xa4a470", + "transactions": [ + "0x204c69c327e3202adba5cfb1e15b99e63fe104905e19a2359d827788f24b0579", + "0x2d07b3bc722c139ffe2ed6a32fc56e944569cc47511fef6e0351dc1da9a23562", + "0x87772fafc7eee41d723a2dcdf2ceaae3726d40a9588ae1f7802f03dce6902fbc", + "0x2e18d4a8bd9b4d0d70651097e44b25f29b4c013ff548ea6e5f3eb975b2bdfb78", + "0xf54ef7af503a7031dc01696339cfcfee3066979a63e3ed626c15bb8282273cea", + "0xb71201da6ad30304942a308e3a7666198394f1916accc9db72d03c1b508c8065", + "0xdc854518c44ae0c3fb80b8b9fdde5da72445552356f79bbfc45d7503a32a23f8", + "0x8b866e254a609a1a4163484cf330bdfc6c6a1878cd35dcc9fbfe2256f324a626", + "0xbc7448bf0c34c0a358ead13e8d3687cbccbbb7fe4048d005cb6648c897bc9254", + "0xa01733dc6d416a59d69fe17dc9d6960dbeb013b0dab2cd59b72cf84b371d19c1", + "0x1c194a1bca34deb14e93e9007de6f971856c43a65208393254f0b2e6f99deab3", + "0x9e9c8a6094300ed29a22892e87ab7fe33d19630ccdd85a0cce72ce6095d0c7da", + "0xa1d6c2fe6a937e437cda199cc0f6891727c0f3ca810a262fb3179fd961cb95c4", + "0xc4b55bada8c0c044f1e8bbd7fb57cd3a46844848e273720fc7bbc757d8e68665", + "0x8e971964ef06896d541d5cefef7cebc79d60d6746aae2fa39e954608e2c49824", + "0x6d120cf3998a767e567ef1b6615e5a14c380103b287c92d1da229cabc49ebb77", + "0x95d1b8d32a80809d79f4a0246e960fec11b59c07f1a33207485dda0b356b3c2c", + "0xa19b4b6372a9c8145e03d62e91536468169350790162508c0f07c66849fde86d", + "0x55348af743327b1377082b9fccddfcdefe7300b65e7ed32575c09d881ece711d", + "0xaaf03a35d70aa96582889565d1211e32fc395c9e63ce82d25cc23518e38aa4bc", + "0x85054ea8eddd5b1e8d010f8aac77693484c5863d3355756a64bd0225124c8fca" + ], + "transactionsRoot": "0x22309b0cc7df445160ca2c6ca344e63296231fad2e9322989477851d38c0eea0", + "uncles": [], + "withdrawals": [ + { + "index": "0x1dfb534", + "validatorIndex": "0xaef81", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1e2a5b" + }, + { + "index": "0x1dfb535", + "validatorIndex": "0xaef82", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1f9526" + }, + { + "index": "0x1dfb536", + "validatorIndex": "0xaef83", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1fa60c" + }, + { + "index": "0x1dfb537", + "validatorIndex": "0xaef84", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1e806a" + }, + { + "index": "0x1dfb538", + "validatorIndex": "0xaef85", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1eb4e4" + }, + { + "index": "0x1dfb539", + "validatorIndex": "0xaef86", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x2054a0" + }, + { + "index": "0x1dfb53a", + "validatorIndex": "0xaef87", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1d984a" + }, + { + "index": "0x1dfb53b", + "validatorIndex": "0xaef88", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1fa4d4" + }, + { + "index": "0x1dfb53c", + "validatorIndex": "0xaef89", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x203a98" + }, + { + "index": "0x1dfb53d", + "validatorIndex": "0xaef8a", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1fec28" + }, + { + "index": "0x1dfb53e", + "validatorIndex": "0xaef8b", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x2025a5" + }, + { + "index": "0x1dfb53f", + "validatorIndex": "0xaef8c", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1fdb08" + }, + { + "index": "0x1dfb540", + "validatorIndex": "0xaef8d", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x200a11" + }, + { + "index": "0x1dfb541", + "validatorIndex": "0xaef8e", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1f03d5" + }, + { + "index": "0x1dfb542", + "validatorIndex": "0xaef8f", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x200804" + }, + { + "index": "0x1dfb543", + "validatorIndex": "0xaef90", + "address": "0xe2e336478e97bfd6a84c0e246f1b8695dd4e990d", + "amount": "0x1dd0bb" + } + ], + "withdrawalsRoot": "0xcba66455c17861d36575f98adedc90b1fc56bbef7982992cab6914528dbd0100" + } + } + """ + }, + "eth_sendRawTransaction" => %{ + arity: 1, + params_validators: [&hex_data_validator/1], + example: """ + {"jsonrpc":"2.0","id": 0,"method":"eth_sendRawTransaction","params":["0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"]} + """, + result: """ + { + "jsonrpc": "2.0", + "id": 0, + "result": "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331" + } + """ + }, + "eth_call" => %{ + arity: 2, + params_validators: [ð_call_validator/1, &block_validator/1], + example: """ + {"jsonrpc":"2.0","id": 0,"method":"eth_call","params":[{"to": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F", "input": "0xd4aae0c4", "from": "0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F"}, "latest"]} + """, + result: """ + { + "jsonrpc": "2.0", + "result": "0x0000000000000000000000001dd91b354ebd706ab3ac7c727455c7baa164945a", + "id": 0 + } + """ } } @@ -94,18 +655,125 @@ defmodule Explorer.EthRPC do 3 => "fourth" } + @incorrect_number_of_params "Incorrect number of params." + + @spec responses([map()]) :: [map()] def responses(requests) do - Enum.map(requests, fn request -> - with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")}, + requests = + requests + |> Enum.with_index() + + proxy_requests = + requests + |> Enum.reduce(%{}, fn {request, index}, acc -> + case proxy_method?(request) do + true -> + Map.put(acc, index, request) + + {:error, _reason} = error -> + Map.put(acc, index, error) + + false -> + acc + end + end) + |> json_rpc() + + Enum.map(requests, fn {request, index} -> + with {:proxy, nil} <- {:proxy, proxy_requests[index]}, + {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")}, {:request, {:ok, result}} <- {:request, do_eth_request(request)} do format_success(result, id) else {:id, :error} -> format_error("id is a required field", 0) {:request, {:error, message}} -> format_error(message, Map.get(request, "id")) + {:proxy, {:error, message}} -> format_error(message, Map.get(request, "id")) + {:proxy, %{result: result}} -> format_success(result, Map.get(request, "id")) + {:proxy, %{error: error}} -> format_error(error, Map.get(request, "id")) end end) end + defp proxy_method?(%{"jsonrpc" => "2.0", "method" => method, "params" => params, "id" => id}) + when is_list(params) and (is_number(id) or is_binary(id) or is_nil(id)) do + with method_definition when not is_nil(method_definition) <- @proxy_methods[method], + {:arity, true} <- {:arity, method_definition[:arity] == length(params)}, + :ok <- validate_params(method_definition[:params_validators], params) do + true + else + {:error, _reason} = error -> + error + + {:arity, false} -> + {:error, @incorrect_number_of_params} + + _ -> + false + end + end + + defp proxy_method?(_), do: false + + defp validate_params(validators, params) do + validators + |> Enum.zip(params) + |> Enum.reduce_while(:ok, fn + {validator_func, param}, :ok -> + {:cont, validator_func.(param)} + + _, error -> + {:halt, error} + end) + end + + defp json_rpc(map) when is_map(map) do + to_request = + Enum.flat_map(Map.values(map), fn + {:error, _} -> + [] + + map when is_map(map) -> + [request_to_elixir(map)] + end) + + with [_ | _] = to_request <- to_request, + {:ok, responses} <- + EthereumJSONRPC.json_rpc(to_request, Application.get_env(:explorer, :json_rpc_named_arguments)) do + {map, []} = + Enum.map_reduce(map, responses, fn + {_index, {:error, _}} = elem, responses -> + {elem, responses} + + {index, _request}, [response | other_responses] -> + {{index, response}, other_responses} + end) + + Enum.into(map, %{}) + else + [] -> + map + + {:error, _reason} = error -> + map + |> Enum.map(fn + {_index, {:error, _}} = elem -> + elem + + {index, _request} -> + {index, error} + end) + |> Enum.into(%{}) + end + end + + defp request_to_elixir(%{"jsonrpc" => json_rpc, "method" => method, "params" => params, "id" => id}) do + %{jsonrpc: json_rpc, method: method, params: params, id: id} + end + + @doc """ + Handles `eth_blockNumber` method + """ + @spec eth_block_number() :: {:ok, String.t()} def eth_block_number do max_block_number = BlockNumber.get_max() @@ -116,6 +784,10 @@ defmodule Explorer.EthRPC do {:ok, max_block_number_hex} end + @doc """ + Handles `eth_getBalance` method + """ + @spec eth_get_balance(String.t(), String.t() | nil) :: {:ok, String.t()} | {:error, String.t()} def eth_get_balance(address_param, block_param \\ nil) do with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)}, {:block, {:ok, block}} <- {:block, block_param(block_param)}, @@ -133,6 +805,126 @@ defmodule Explorer.EthRPC do end end + @doc """ + Handles `eth_gasPrice` method + """ + @spec eth_gas_price() :: {:ok, String.t()} | {:error, String.t()} + def eth_gas_price do + case GasPriceOracle.get_gas_prices() do + {:ok, gas_prices} -> + {:ok, Wei.hex_format(gas_prices[:average][:wei])} + + _ -> + {:error, @nil_gas_price_message} + end + end + + @doc """ + Handles `eth_maxPriorityFeePerGas` method + """ + @spec eth_max_priority_fee_per_gas() :: {:ok, String.t()} | {:error, String.t()} + def eth_max_priority_fee_per_gas do + case GasPriceOracle.get_gas_prices() do + {:ok, gas_prices} -> + {:ok, Wei.hex_format(gas_prices[:average][:priority_fee_wei])} + + _ -> + {:error, @nil_gas_price_message} + end + end + + @doc """ + Handles `eth_chainId` method + """ + @spec eth_chain_id() :: {:ok, String.t() | nil} + def eth_chain_id do + {:ok, chain_id()} + end + + @doc """ + Handles `eth_getTransactionByHash` method + """ + @spec eth_get_transaction_by_hash(String.t()) :: {:ok, map() | nil} | {:error, String.t()} + def eth_get_transaction_by_hash(transaction_hash_string) do + validate_and_render_transaction(transaction_hash_string, &render_transaction/1, api?: true) + end + + defp render_transaction(transaction) do + {:ok, + %{ + "blockHash" => transaction.block_hash, + "blockNumber" => encode_quantity(transaction.block_number), + "from" => transaction.from_address_hash, + "gas" => encode_quantity(transaction.gas), + "gasPrice" => transaction.gas_price |> Wei.to(:wei) |> encode_quantity(), + "maxPriorityFeePerGas" => transaction.max_priority_fee_per_gas |> Wei.to(:wei) |> encode_quantity(), + "maxFeePerGas" => transaction.max_fee_per_gas |> Wei.to(:wei) |> encode_quantity(), + "hash" => transaction.hash, + "input" => transaction.input, + "nonce" => encode_quantity(transaction.nonce), + "to" => transaction.to_address_hash, + "transactionIndex" => encode_quantity(transaction.index), + "value" => transaction.value |> Wei.to(:wei) |> encode_quantity(), + "type" => encode_quantity(transaction.type), + "chainId" => chain_id(), + "v" => encode_quantity(transaction.v), + "r" => encode_quantity(transaction.r), + "s" => encode_quantity(transaction.s) + }} + end + + @doc """ + Handles `eth_getTransactionReceipt` method + """ + @spec eth_get_transaction_receipt(String.t()) :: {:ok, map() | nil} | {:error, String.t()} + def eth_get_transaction_receipt(transaction_hash_string) do + necessity_by_association = %{block: :optional, logs: :optional} + + validate_and_render_transaction(transaction_hash_string, &render_transaction_receipt/1, + api?: true, + necessity_by_association: necessity_by_association + ) + end + + defp render_transaction_receipt(transaction) do + {:ok, status} = Status.dump(transaction.status) + + {:ok, + %{ + "blockHash" => transaction.block_hash, + "blockNumber" => encode_quantity(transaction.block_number), + "contractAddress" => transaction.created_contract_address_hash, + "cumulativeGasUsed" => encode_quantity(transaction.cumulative_gas_used), + "effectiveGasPrice" => + (transaction.gas_price || transaction |> Transaction.effective_gas_price()) + |> Wei.to(:wei) + |> encode_quantity(), + "from" => transaction.from_address_hash, + "gasUsed" => encode_quantity(transaction.gas_used), + "logs" => Enum.map(transaction.logs, &render_log(&1, transaction)), + 'logsBloom' => "0x" <> (transaction.logs |> BloomFilter.logs_bloom() |> Base.encode16(case: :lower)), + "status" => encode_quantity(status), + "to" => transaction.to_address_hash, + "transactionHash" => transaction.hash, + "transactionIndex" => encode_quantity(transaction.index), + "type" => encode_quantity(transaction.type) + }} + end + + defp validate_and_render_transaction(transaction_hash_string, render_func, params) do + with {:transaction_hash, {:ok, transaction_hash}} <- + {:transaction_hash, Chain.string_to_transaction_hash(transaction_hash_string)}, + {:transaction, {:ok, transaction}} <- {:transaction, Chain.hash_to_transaction(transaction_hash, params)} do + render_func.(transaction) + else + {:transaction_hash, :error} -> + {:error, "Transaction hash is invalid"} + + {:transaction, _} -> + {:ok, nil} + end + end + def eth_get_logs(filter_options) do with {:ok, address_or_topic_params} <- address_or_topic_params(filter_options), {:ok, from_block_param, to_block_param} <- logs_blocks_filter(filter_options), @@ -164,27 +956,58 @@ defmodule Explorer.EthRPC do end defp render_log(log) do - topics = - Enum.reject( - [log.first_topic, log.second_topic, log.third_topic, log.fourth_topic], - &is_nil/1 - ) + topics = prepare_topics(log) %{ "address" => to_string(log.address_hash), "blockHash" => to_string(log.block_hash), - "blockNumber" => Integer.to_string(log.block_number, 16), + "blockNumber" => + log.block_number + |> encode_quantity(), "data" => to_string(log.data), - "logIndex" => Integer.to_string(log.index, 16), + "logIndex" => + log.index + |> encode_quantity(), "removed" => log.block_consensus == false, "topics" => topics, "transactionHash" => to_string(log.transaction_hash), - "transactionIndex" => log.transaction_index, - "transactionLogIndex" => log.index, - "type" => "mined" + "transactionIndex" => + log.transaction_index + |> encode_quantity() + } + end + + defp render_log(log, transaction) do + topics = prepare_topics(log) + + %{ + "address" => log.address_hash, + "blockHash" => log.block_hash, + "blockNumber" => encode_quantity(log.block_number), + "data" => log.data, + "logIndex" => encode_quantity(log.index), + "removed" => transaction_consensus(transaction) == false, + "topics" => topics, + "transactionHash" => log.transaction_hash, + "transactionIndex" => encode_quantity(transaction.index) } end + defp transaction_consensus(transaction) do + if DenormalizationHelper.transactions_denormalization_finished?() do + transaction.block_consensus + else + transaction.block.consensus + end + end + + defp prepare_topics(log) do + Enum.reject( + [log.first_topic, log.second_topic, log.third_topic, log.fourth_topic], + &is_nil/1 + ) + end + defp cast_block("0x" <> hexadecimal_digits = input) do case Integer.parse(hexadecimal_digits, 16) do {integer, ""} -> {:ok, integer} @@ -425,6 +1248,8 @@ defmodule Explorer.EthRPC do defp block_param(nil), do: {:ok, :latest} defp block_param(_), do: :error + def encode_quantity(%Decimal{} = decimal), do: encode_quantity(Decimal.to_integer(decimal)) + def encode_quantity(binary) when is_binary(binary) do hex_binary = Base.encode16(binary, case: :lower) @@ -446,4 +1271,6 @@ defmodule Explorer.EthRPC do end def methods, do: @methods + + defp chain_id, do: :block_scout_web |> Application.get_env(:chain_id) |> Helper.parse_integer() |> encode_quantity() end diff --git a/apps/explorer/lib/explorer/eth_rpc_helper.ex b/apps/explorer/lib/explorer/eth_rpc_helper.ex new file mode 100644 index 000000000000..2cecf52be106 --- /dev/null +++ b/apps/explorer/lib/explorer/eth_rpc_helper.ex @@ -0,0 +1,121 @@ +defmodule Explorer.EthRpcHelper do + @moduledoc """ + Helper module for Explorer.EthRPC. Mostly contains functions to validate input args + """ + + alias Explorer.Chain.{Data, Hash.Address} + alias Explorer.Chain.Hash.Full, as: Hash + + @invalid_address "Invalid address" + @invalid_block_number "Invalid block number" + @invalid_integer "Invalid integer" + @missed_to_address "Missed `to` address" + @invalid_bool "Invalid bool" + @invalid_hash "Invalid hash" + @invalid_hex_data "Invalid hex data" + @doc """ + Validates if address is valid + """ + @spec address_hash_validator(binary(), String.t()) :: :ok | {:error, String.t()} + def address_hash_validator(address_hash, message \\ @invalid_address) do + case Address.cast(address_hash) do + {:ok, _} -> :ok + :error -> {:error, message} + end + end + + @doc """ + Validates if hash is valid + """ + @spec hash_validator(binary()) :: :ok | {:error, String.t()} + def hash_validator(hash) do + case Hash.cast(hash) do + {:ok, _} -> :ok + :error -> {:error, @invalid_hash} + end + end + + @doc """ + Validates if hex data is valid + """ + @spec hex_data_validator(binary()) :: :ok | {:error, String.t()} + def hex_data_validator(hex_data) do + case Data.cast(hex_data) do + {:ok, _} -> :ok + _ -> {:error, @invalid_hex_data} + end + end + + @doc """ + Validates if block is valid + """ + @spec block_validator(binary()) :: :ok | {:error, String.t()} + def block_validator(block_tag) when block_tag in ["latest", "earliest", "pending"], do: :ok + + def block_validator(block_number) do + parse_integer(block_number) || {:error, @invalid_block_number} + end + + def integer_validator(hex) do + parse_integer(hex) || {:error, @invalid_integer} + end + + @doc """ + Validates eth_call map + """ + @spec eth_call_validator(map()) :: :ok | {:error, String.t()} + def eth_call_validator(%{"to" => to_address} = eth_call) do + with :ok <- address_hash_validator(to_address, "Invalid `to` address"), + :ok <- validate_optional_address(eth_call["from"], "from"), + :ok <- validate_optional_integer(eth_call["gas"], "gas"), + :ok <- validate_optional_integer(eth_call["gasPrice"], "gasPrice"), + :ok <- validate_optional_integer(eth_call["value"], "value"), + :ok <- validate_optional_hex_data(eth_call["input"], "input") do + :ok + else + error -> + error + end + end + + def eth_call_validator(_), do: {:error, @missed_to_address} + + @doc """ + Validates if bool is valid + """ + @spec bool_validator(boolean()) :: :ok | {:error, String.t()} + def bool_validator(bool) when is_boolean(bool), do: :ok + def bool_validator(_), do: {:error, @invalid_bool} + + defp validate_optional_address(nil, _), do: :ok + + defp validate_optional_address(address_hash, field_name) do + address_hash_validator(address_hash, "Invalid `#{field_name}` address") + end + + defp validate_optional_integer(nil, _), do: :ok + + defp validate_optional_integer(integer, field_name) do + parse_integer(integer) || {:error, "Invalid `#{field_name}` quantity"} + end + + defp parse_integer("0x"), do: :ok + + defp parse_integer("0x" <> hex_integer) do + case Integer.parse(hex_integer, 16) do + {_integer, ""} -> :ok + _ -> nil + end + end + + defp parse_integer(_), do: nil + + defp validate_optional_hex_data(nil, _), do: :ok + + defp validate_optional_hex_data(data, field_name) do + case Data.cast(data) do + {:ok, _} -> :ok + _ -> {:error, "Invalid `#{field_name}` data"} + end + end +end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 4663c3c9b9db..2395fd95aa9f 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -3,21 +3,23 @@ defmodule Explorer.Etherscan do The etherscan context. """ - import Ecto.Query, only: [from: 2, where: 3, or_where: 3, union: 2, subquery: 1, order_by: 3] + import Ecto.Query, + only: [from: 2, where: 3, union: 2, subquery: 1, order_by: 3, limit: 2, offset: 2, preload: 3] + import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] alias Explorer.Etherscan.Logs alias Explorer.{Chain, Repo} alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} - alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, TokenTransfer, Transaction} + alias Explorer.Chain.{Address, Block, DenormalizationHelper, Hash, InternalTransaction, TokenTransfer, Transaction} alias Explorer.Chain.Transaction.History.TransactionStats @default_options %{ order_by_direction: :desc, page_number: 1, page_size: 10_000, - start_block: nil, - end_block: nil, + startblock: nil, + endblock: nil, start_timestamp: nil, end_timestamp: nil } @@ -101,18 +103,33 @@ defmodule Explorer.Etherscan do @spec list_internal_transactions(Hash.Full.t()) :: [map()] def list_internal_transactions(%Hash{byte_count: unquote(Hash.Full.byte_count())} = transaction_hash) do query = - from( - it in InternalTransaction, - inner_join: t in assoc(it, :transaction), - inner_join: b in assoc(t, :block), - where: it.transaction_hash == ^transaction_hash, - limit: 10_000, - select: - merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: b.timestamp, - block_number: b.number - }) - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + it in InternalTransaction, + inner_join: transaction in assoc(it, :transaction), + where: not is_nil(transaction.block_hash), + where: it.transaction_hash == ^transaction_hash, + limit: 10_000, + select: + merge(map(it, ^@internal_transaction_fields), %{ + block_timestamp: transaction.block_timestamp, + block_number: transaction.block_number + }) + ) + else + from( + it in InternalTransaction, + inner_join: t in assoc(it, :transaction), + inner_join: b in assoc(t, :block), + where: it.transaction_hash == ^transaction_hash, + limit: 10_000, + select: + merge(map(it, ^@internal_transaction_fields), %{ + block_timestamp: b.timestamp, + block_number: b.number + }) + ) + end query |> Chain.where_transaction_has_multiple_internal_transactions() @@ -158,8 +175,8 @@ defmodule Explorer.Etherscan do query = from( it in InternalTransaction, - inner_join: b in subquery(consensus_blocks), - on: it.block_number == b.number, + inner_join: block in subquery(consensus_blocks), + on: it.block_number == block.number, order_by: [ {^options.order_by_direction, it.block_number}, {:desc, it.transaction_index}, @@ -169,8 +186,8 @@ defmodule Explorer.Etherscan do offset: ^offset(options), select: merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: b.timestamp, - block_number: b.number + block_timestamp: block.timestamp, + block_number: block.number }) ) @@ -212,19 +229,35 @@ defmodule Explorer.Etherscan do |> Repo.replica().all() else query = - from( - it in InternalTransaction, - inner_join: t in assoc(it, :transaction), - inner_join: b in assoc(t, :block), - order_by: [{^options.order_by_direction, t.block_number}], - limit: ^options.page_size, - offset: ^offset(options), - select: - merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: b.timestamp, - block_number: b.number - }) - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + it in InternalTransaction, + inner_join: transaction in assoc(it, :transaction), + where: not is_nil(transaction.block_hash), + order_by: [{^options.order_by_direction, transaction.block_number}], + limit: ^options.page_size, + offset: ^offset(options), + select: + merge(map(it, ^@internal_transaction_fields), %{ + block_timestamp: transaction.block_timestamp, + block_number: transaction.block_number + }) + ) + else + from( + it in InternalTransaction, + inner_join: t in assoc(it, :transaction), + inner_join: b in assoc(t, :block), + order_by: [{^options.order_by_direction, t.block_number}], + limit: ^options.page_size, + offset: ^offset(options), + select: + merge(map(it, ^@internal_transaction_fields), %{ + block_timestamp: b.timestamp, + block_number: b.number + }) + ) + end query |> Chain.where_transaction_has_multiple_internal_transactions() @@ -257,6 +290,50 @@ defmodule Explorer.Etherscan do end end + @doc """ + Gets a list of ERC-721 token transfers for a given address_hash. If contract_address_hash is not nil, transfers will be filtered by contract. + """ + @spec list_nft_transfers(Hash.Address.t(), Hash.Address.t() | nil, map()) :: [TokenTransfer.t()] + def list_nft_transfers( + %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash, + contract_address_hash, + options \\ @default_options + ) do + options + |> base_nft_transfers_query(contract_address_hash) + |> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash) + |> Repo.replica().all() + end + + @doc """ + Gets a list of ERC-721 token transfers for a given token contract_address_hash. + """ + @spec list_nft_transfers_by_token(Hash.Address.t(), map()) :: [TokenTransfer.t()] + def list_nft_transfers_by_token( + %Hash{byte_count: unquote(Hash.Address.byte_count())} = contract_address_hash, + options \\ @default_options + ) do + options + |> base_nft_transfers_query(contract_address_hash) + |> Repo.replica().all() + end + + defp base_nft_transfers_query(options, contract_address_hash) do + options = Map.merge(@default_options, options) + + TokenTransfer.erc_721_token_transfers_query() + |> where_contract_address_match(contract_address_hash) + |> order_by([tt], [ + {^options.order_by_direction, tt.block_number}, + {^options.order_by_direction, tt.log_index} + ]) + |> where_start_block_match_tt(options) + |> where_end_block_match_tt(options) + |> limit(^options.page_size) + |> offset(^offset(options)) + |> preload([block: block], [{:block, block}, :transaction]) + end + @doc """ Gets a list of blocks mined by `t:Explorer.Chain.Hash.Address.t/0`. @@ -279,14 +356,14 @@ defmodule Explorer.Etherscan do query = from( - b in Block, - where: b.miner_hash == ^address_hash, - order_by: [desc: b.number], + block in Block, + where: block.miner_hash == ^address_hash, + order_by: [desc: block.number], limit: ^merged_options.page_size, offset: ^offset(merged_options), select: %{ - number: b.number, - timestamp: b.timestamp + number: block.number, + timestamp: block.timestamp } ) @@ -343,6 +420,8 @@ defmodule Explorer.Etherscan do @transaction_fields ~w( block_hash block_number + block_consensus + block_timestamp created_contract_address_hash cumulative_gas_used from_address_hash @@ -393,23 +472,39 @@ defmodule Explorer.Etherscan do defp list_transactions(address_hash, max_block_number, options) do query = - from( - t in Transaction, - inner_join: b in assoc(t, :block), - order_by: [{^options.order_by_direction, t.block_number}], - limit: ^options.page_size, - offset: ^offset(options), - select: - merge(map(t, ^@transaction_fields), %{ - block_timestamp: b.timestamp, - confirmations: fragment("? - ?", ^max_block_number, t.block_number) - }) - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + t in Transaction, + where: not is_nil(t.block_hash), + where: t.block_consensus == true, + order_by: [{^options.order_by_direction, t.block_number}], + limit: ^options.page_size, + offset: ^offset(options), + select: + merge(map(t, ^@transaction_fields), %{ + confirmations: fragment("? - ?", ^max_block_number, t.block_number) + }) + ) + else + from( + t in Transaction, + inner_join: b in assoc(t, :block), + where: b.consensus == true, + order_by: [{^options.order_by_direction, t.block_number}], + limit: ^options.page_size, + offset: ^offset(options), + select: + merge(map(t, ^@transaction_fields), %{ + block_timestamp: b.timestamp, + confirmations: fragment("? - ?", ^max_block_number, t.block_number) + }) + ) + end query |> where_address_match(address_hash, options) - |> where_start_block_match(options) - |> where_end_block_match(options) + |> where_start_transaction_block_match(options) + |> where_end_transaction_block_match(options) |> where_start_timestamp_match(options) |> where_end_timestamp_match(options) |> Repo.replica().all() @@ -424,15 +519,18 @@ defmodule Explorer.Etherscan do end defp where_address_match(query, address_hash, _) do - query - |> where([t], t.to_address_hash == ^address_hash) - |> or_where([t], t.from_address_hash == ^address_hash) - |> or_where([t], t.created_contract_address_hash == ^address_hash) + where( + query, + [t], + t.to_address_hash == ^address_hash or t.from_address_hash == ^address_hash or + t.created_contract_address_hash == ^address_hash + ) end @token_transfer_fields ~w( block_number block_hash + block_consensus token_contract_address_hash transaction_hash from_address_hash @@ -464,76 +562,153 @@ defmodule Explorer.Etherscan do tt_specific_token_query = tt_query + |> where_start_block_match_tt(options) + |> where_end_block_match_tt(options) |> where_contract_address_match(contract_address_hash) wrapped_query = - from( - tt in subquery(tt_specific_token_query), - inner_join: t in Transaction, - on: tt.transaction_hash == t.hash and tt.block_number == t.block_number and tt.block_hash == t.block_hash, - inner_join: b in assoc(t, :block), - order_by: [{^options.order_by_direction, tt.block_number}, {^options.order_by_direction, tt.token_log_index}], - select: %{ - token_contract_address_hash: tt.token_contract_address_hash, - transaction_hash: tt.transaction_hash, - from_address_hash: tt.from_address_hash, - to_address_hash: tt.to_address_hash, - amount: tt.amount, - amounts: tt.amounts, - transaction_nonce: t.nonce, - transaction_index: t.index, - transaction_gas: t.gas, - transaction_gas_price: t.gas_price, - transaction_gas_used: t.gas_used, - transaction_cumulative_gas_used: t.cumulative_gas_used, - transaction_input: t.input, - block_hash: b.hash, - block_number: b.number, - block_timestamp: b.timestamp, - confirmations: fragment("? - ?", ^block_height, t.block_number), - token_ids: tt.token_ids, - token_name: tt.token_name, - token_symbol: tt.token_symbol, - token_decimals: tt.token_decimals, - token_type: tt.token_type, - token_log_index: tt.token_log_index - } - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + tt in subquery(tt_specific_token_query), + inner_join: t in Transaction, + on: + tt.transaction_hash == t.hash and tt.block_number == t.block_number and tt.block_hash == t.block_hash and + t.block_consensus == true, + order_by: [{^options.order_by_direction, tt.block_number}, {^options.order_by_direction, tt.token_log_index}], + select: %{ + token_contract_address_hash: tt.token_contract_address_hash, + transaction_hash: tt.transaction_hash, + from_address_hash: tt.from_address_hash, + to_address_hash: tt.to_address_hash, + amount: tt.amount, + amounts: tt.amounts, + transaction_nonce: t.nonce, + transaction_index: t.index, + transaction_gas: t.gas, + transaction_gas_price: t.gas_price, + transaction_gas_used: t.gas_used, + transaction_cumulative_gas_used: t.cumulative_gas_used, + transaction_input: t.input, + block_hash: t.block_hash, + block_number: t.block_number, + block_timestamp: t.block_timestamp, + confirmations: fragment("? - ?", ^block_height, t.block_number), + token_ids: tt.token_ids, + token_name: tt.token_name, + token_symbol: tt.token_symbol, + token_decimals: tt.token_decimals, + token_type: tt.token_type, + token_log_index: tt.token_log_index + } + ) + else + from( + tt in subquery(tt_specific_token_query), + inner_join: t in Transaction, + on: tt.transaction_hash == t.hash and tt.block_number == t.block_number and tt.block_hash == t.block_hash, + inner_join: b in assoc(t, :block), + where: b.consensus == true, + order_by: [{^options.order_by_direction, tt.block_number}, {^options.order_by_direction, tt.token_log_index}], + select: %{ + token_contract_address_hash: tt.token_contract_address_hash, + transaction_hash: tt.transaction_hash, + from_address_hash: tt.from_address_hash, + to_address_hash: tt.to_address_hash, + amount: tt.amount, + amounts: tt.amounts, + transaction_nonce: t.nonce, + transaction_index: t.index, + transaction_gas: t.gas, + transaction_gas_price: t.gas_price, + transaction_gas_used: t.gas_used, + transaction_cumulative_gas_used: t.cumulative_gas_used, + transaction_input: t.input, + block_hash: b.hash, + block_number: b.number, + block_timestamp: b.timestamp, + confirmations: fragment("? - ?", ^block_height, t.block_number), + token_ids: tt.token_ids, + token_name: tt.token_name, + token_symbol: tt.token_symbol, + token_decimals: tt.token_decimals, + token_type: tt.token_type, + token_log_index: tt.token_log_index + } + ) + end wrapped_query - |> where_start_block_match(options) - |> where_end_block_match(options) |> Repo.replica().all() end - defp where_start_block_match(query, %{start_block: nil}), do: query + defp where_start_block_match(query, %{startblock: nil}), do: query - defp where_start_block_match(query, %{start_block: start_block}) do + defp where_start_block_match(query, %{startblock: start_block}) do where(query, [..., block], block.number >= ^start_block) end - defp where_end_block_match(query, %{end_block: nil}), do: query + defp where_end_block_match(query, %{endblock: nil}), do: query - defp where_end_block_match(query, %{end_block: end_block}) do + defp where_end_block_match(query, %{endblock: end_block}) do where(query, [..., block], block.number <= ^end_block) end + defp where_start_transaction_block_match(query, %{startblock: nil}), do: query + + defp where_start_transaction_block_match(query, %{startblock: start_block} = params) do + if DenormalizationHelper.transactions_denormalization_finished?() do + where(query, [transaction], transaction.block_number >= ^start_block) + else + where_start_block_match(query, params) + end + end + + defp where_end_transaction_block_match(query, %{endblock: nil}), do: query + + defp where_end_transaction_block_match(query, %{endblock: end_block} = params) do + if DenormalizationHelper.transactions_denormalization_finished?() do + where(query, [transaction], transaction.block_number <= ^end_block) + else + where_end_block_match(query, params) + end + end + + defp where_start_block_match_tt(query, %{startblock: nil}), do: query + + defp where_start_block_match_tt(query, %{startblock: start_block}) do + where(query, [tt], tt.block_number >= ^start_block) + end + + defp where_end_block_match_tt(query, %{endblock: nil}), do: query + + defp where_end_block_match_tt(query, %{endblock: end_block}) do + where(query, [tt], tt.block_number <= ^end_block) + end + defp where_start_timestamp_match(query, %{start_timestamp: nil}), do: query defp where_start_timestamp_match(query, %{start_timestamp: start_timestamp}) do - where(query, [..., block], ^start_timestamp <= block.timestamp) + if DenormalizationHelper.transactions_denormalization_finished?() do + where(query, [transaction], ^start_timestamp <= transaction.block_timestamp) + else + where(query, [..., block], ^start_timestamp <= block.timestamp) + end end defp where_end_timestamp_match(query, %{end_timestamp: nil}), do: query defp where_end_timestamp_match(query, %{end_timestamp: end_timestamp}) do - where(query, [..., block], block.timestamp <= ^end_timestamp) + if DenormalizationHelper.transactions_denormalization_finished?() do + where(query, [transaction], transaction.block_timestamp <= ^end_timestamp) + else + where(query, [..., block], block.timestamp <= ^end_timestamp) + end end defp where_contract_address_match(query, nil), do: query defp where_contract_address_match(query, contract_address_hash) do - where(query, [tt, _], tt.token_contract_address_hash == ^contract_address_hash) + where(query, [tt], tt.token_contract_address_hash == ^contract_address_hash) end defp offset(options), do: (options.page_number - 1) * options.page_size diff --git a/apps/explorer/lib/explorer/etherscan/blocks.ex b/apps/explorer/lib/explorer/etherscan/blocks.ex index d7238fb7a20f..0249f46743d3 100644 --- a/apps/explorer/lib/explorer/etherscan/blocks.ex +++ b/apps/explorer/lib/explorer/etherscan/blocks.ex @@ -11,7 +11,7 @@ defmodule Explorer.Etherscan.Blocks do ] alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Address.CoinBalance, Block, Hash, Wei} + alias Explorer.Chain.{Address, Address.CoinBalance, Block, Hash, Wei} @doc """ Returns the balance of the given address and block combination. @@ -39,12 +39,16 @@ defmodule Explorer.Etherscan.Blocks do end def get_balance_as_of_block(address, :latest) do - case Chain.max_consensus_block_number() do - {:ok, latest_block_number} -> - get_balance_as_of_block(address, latest_block_number) + latest_coin_balance_query = + from( + a in Address, + select: a.fetched_coin_balance, + where: a.hash == ^address + ) - {:error, :not_found} -> - {:error, :not_found} + case Repo.replica().one(latest_coin_balance_query) do + nil -> {:error, :not_found} + coin_balance -> {:ok, coin_balance} end end diff --git a/apps/explorer/lib/explorer/etherscan/contracts.ex b/apps/explorer/lib/explorer/etherscan/contracts.ex index 4985220bf64f..72975cfb5a21 100644 --- a/apps/explorer/lib/explorer/etherscan/contracts.ex +++ b/apps/explorer/lib/explorer/etherscan/contracts.ex @@ -11,11 +11,16 @@ defmodule Explorer.Etherscan.Contracts do where: 3 ] - alias Explorer.{Chain, Repo} + alias Explorer.Repo alias Explorer.Chain.{Address, Hash, SmartContract} + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.EIP1167 + @doc """ + Returns address with preloaded SmartContract and proxy info if it exists + """ @spec address_hash_to_address_with_source_code(Hash.Address.t()) :: Address.t() | nil - def address_hash_to_address_with_source_code(address_hash) do + def address_hash_to_address_with_source_code(address_hash, twin_needed? \\ true) do result = case Repo.replica().get(Address, address_hash) do nil -> @@ -24,9 +29,8 @@ defmodule Explorer.Etherscan.Contracts do address -> address_with_smart_contract = Repo.replica().preload(address, [ - :smart_contract, - :decompiled_smart_contracts, - :smart_contract_additional_sources + [smart_contract: :smart_contract_additional_sources], + :decompiled_smart_contracts ]) if address_with_smart_contract.smart_contract do @@ -38,8 +42,7 @@ defmodule Explorer.Etherscan.Contracts do } else address_verified_twin_contract = - Chain.get_minimal_proxy_template(address_hash) || - Chain.get_address_verified_twin_contract(address_hash).verified_contract + EIP1167.get_implementation_address(address_hash) || maybe_fetch_twin(twin_needed?, address_hash) compose_address_with_smart_contract( address_with_smart_contract, @@ -52,6 +55,9 @@ defmodule Explorer.Etherscan.Contracts do |> append_proxy_info() end + defp maybe_fetch_twin(twin_needed?, address_hash), + do: if(twin_needed?, do: SmartContract.get_address_verified_twin_contract(address_hash).verified_contract) + defp compose_address_with_smart_contract(address_with_smart_contract, address_verified_twin_contract) do if address_verified_twin_contract do formatted_code = format_source_code_output(address_verified_twin_contract) @@ -67,7 +73,7 @@ defmodule Explorer.Etherscan.Contracts do def append_proxy_info(%Address{smart_contract: smart_contract} = address) when not is_nil(smart_contract) do updated_smart_contract = - if SmartContract.proxy_contract?(smart_contract) do + if Proxy.proxy_contract?(smart_contract) do smart_contract |> Map.put(:is_proxy, true) |> Map.put( diff --git a/apps/explorer/lib/explorer/etherscan/logs.ex b/apps/explorer/lib/explorer/etherscan/logs.ex index c7ae91e34732..c3b79a439fc8 100644 --- a/apps/explorer/lib/explorer/etherscan/logs.ex +++ b/apps/explorer/lib/explorer/etherscan/logs.ex @@ -5,10 +5,10 @@ defmodule Explorer.Etherscan.Logs do """ - import Ecto.Query, only: [from: 2, where: 3, subquery: 1, order_by: 3, union: 2] + import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, union: 2] alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, InternalTransaction, Log, Transaction} + alias Explorer.Chain.{Block, DenormalizationHelper, InternalTransaction, Log, Transaction} @base_filter %{ from_block: nil, @@ -34,11 +34,10 @@ defmodule Explorer.Etherscan.Logs do :fourth_topic, :index, :address_hash, - :transaction_hash, - :type + :transaction_hash ] - @default_paging_options %{block_number: nil, transaction_index: nil, log_index: nil} + @default_paging_options %{block_number: nil, log_index: nil} @doc """ Gets a list of logs that meet the criteria in a given filter map. @@ -76,77 +75,126 @@ defmodule Explorer.Etherscan.Logs do paging_options = if is_nil(paging_options), do: @default_paging_options, else: paging_options prepared_filter = Map.merge(@base_filter, filter) - logs_query = where_topic_match(Log, prepared_filter) - - query_to_address_hash_wrapped = - logs_query - |> internal_transaction_query(:to_address_hash, prepared_filter, address_hash) - |> Chain.wrapped_union_subquery() - - query_from_address_hash_wrapped = - logs_query - |> internal_transaction_query(:from_address_hash, prepared_filter, address_hash) - |> Chain.wrapped_union_subquery() - - query_created_contract_address_hash_wrapped = - logs_query - |> internal_transaction_query(:created_contract_address_hash, prepared_filter, address_hash) - |> Chain.wrapped_union_subquery() - - internal_transaction_log_query = - query_to_address_hash_wrapped - |> union(^query_from_address_hash_wrapped) - |> union(^query_created_contract_address_hash_wrapped) + if DenormalizationHelper.transactions_denormalization_finished?() do + logs_query = + Log + |> where_topic_match(prepared_filter) + |> where([log], log.address_hash == ^address_hash) + |> where([log], log.block_number >= ^prepared_filter.from_block) + |> where([log], log.block_number <= ^prepared_filter.to_block) + |> limit(1000) + |> order_by([log], asc: log.block_number, asc: log.index) + |> page_logs(paging_options) + + all_transaction_logs_query = + from(log in subquery(logs_query), + join: transaction in Transaction, + on: log.transaction_hash == transaction.hash, + select: map(log, ^@log_fields), + select_merge: %{ + gas_price: transaction.gas_price, + gas_used: transaction.gas_used, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + block_timestamp: transaction.block_timestamp, + block_consensus: transaction.block_consensus + } + ) - all_transaction_logs_query = - from(transaction in Transaction, - join: log in ^logs_query, - on: log.transaction_hash == transaction.hash, - where: transaction.block_number >= ^prepared_filter.from_block, - where: transaction.block_number <= ^prepared_filter.to_block, - where: - transaction.to_address_hash == ^address_hash or - transaction.from_address_hash == ^address_hash or - transaction.created_contract_address_hash == ^address_hash, - select: map(log, ^@log_fields), - select_merge: %{ - gas_price: transaction.gas_price, - gas_used: transaction.gas_used, - transaction_index: transaction.index, - block_number: transaction.block_number - }, - union: ^internal_transaction_log_query - ) + query_with_blocks = + from(log_transaction_data in subquery(all_transaction_logs_query), + where: log_transaction_data.address_hash == ^address_hash, + order_by: log_transaction_data.block_number, + select_merge: %{ + block_consensus: log_transaction_data.block_consensus + } + ) - query_with_blocks = - from(log_transaction_data in subquery(all_transaction_logs_query), - join: block in Block, - on: block.number == log_transaction_data.block_number, - where: log_transaction_data.address_hash == ^address_hash, - order_by: block.number, - limit: 1000, - select_merge: %{ - transaction_index: log_transaction_data.transaction_index, - block_hash: block.hash, - block_number: block.number, - block_timestamp: block.timestamp, - block_consensus: block.consensus - } - ) + query_with_consensus = + if Map.get(filter, :allow_non_consensus) do + query_with_blocks + else + from([transaction] in query_with_blocks, + where: transaction.block_consensus == true + ) + end + + query_with_consensus + |> Repo.replica().all() + else + logs_query = where_topic_match(Log, prepared_filter) + + query_to_address_hash_wrapped = + logs_query + |> internal_transaction_query(:to_address_hash, prepared_filter, address_hash) + |> Chain.wrapped_union_subquery() + + query_from_address_hash_wrapped = + logs_query + |> internal_transaction_query(:from_address_hash, prepared_filter, address_hash) + |> Chain.wrapped_union_subquery() + + query_created_contract_address_hash_wrapped = + logs_query + |> internal_transaction_query(:created_contract_address_hash, prepared_filter, address_hash) + |> Chain.wrapped_union_subquery() + + internal_transaction_log_query = + query_to_address_hash_wrapped + |> union(^query_from_address_hash_wrapped) + |> union(^query_created_contract_address_hash_wrapped) + + all_transaction_logs_query = + from(transaction in Transaction, + join: log in ^logs_query, + on: log.transaction_hash == transaction.hash, + where: transaction.block_number >= ^prepared_filter.from_block, + where: transaction.block_number <= ^prepared_filter.to_block, + where: + transaction.to_address_hash == ^address_hash or + transaction.from_address_hash == ^address_hash or + transaction.created_contract_address_hash == ^address_hash, + select: map(log, ^@log_fields), + select_merge: %{ + gas_price: transaction.gas_price, + gas_used: transaction.gas_used, + transaction_index: transaction.index, + block_number: transaction.block_number + }, + union: ^internal_transaction_log_query + ) - query_with_consensus = - if Map.get(filter, :allow_non_consensus) do - query_with_blocks - else - from([_, block] in query_with_blocks, - where: block.consensus == true + query_with_blocks = + from(log_transaction_data in subquery(all_transaction_logs_query), + join: block in Block, + on: block.number == log_transaction_data.block_number, + where: log_transaction_data.address_hash == ^address_hash, + order_by: block.number, + limit: 1000, + select_merge: %{ + transaction_index: log_transaction_data.transaction_index, + block_hash: block.hash, + block_number: block.number, + block_timestamp: block.timestamp, + block_consensus: block.consensus + } ) - end - query_with_consensus - |> order_by([log], asc: log.index) - |> page_logs(paging_options) - |> Repo.replica().all() + query_with_consensus = + if Map.get(filter, :allow_non_consensus) do + query_with_blocks + else + from([_, block] in query_with_blocks, + where: block.consensus == true + ) + end + + query_with_consensus + |> order_by([log], asc: log.index) + |> page_logs(paging_options) + |> Repo.replica().all() + end end # Since address_hash was not present, we know that a @@ -156,49 +204,90 @@ defmodule Explorer.Etherscan.Logs do def list_logs(filter, paging_options) do paging_options = if is_nil(paging_options), do: @default_paging_options, else: paging_options prepared_filter = Map.merge(@base_filter, filter) - logs_query = where_topic_match(Log, prepared_filter) - block_transaction_query = - from(transaction in Transaction, - join: block in assoc(transaction, :block), - where: block.number >= ^prepared_filter.from_block, - where: block.number <= ^prepared_filter.to_block, - select: %{ - transaction_hash: transaction.hash, - gas_price: transaction.gas_price, - gas_used: transaction.gas_used, - transaction_index: transaction.index, - block_hash: block.hash, - block_number: block.number, - block_timestamp: block.timestamp, - block_consensus: block.consensus - } - ) + if DenormalizationHelper.transactions_denormalization_finished?() do + block_transaction_query = + from(transaction in Transaction, + where: transaction.block_number >= ^prepared_filter.from_block, + where: transaction.block_number <= ^prepared_filter.to_block, + select: %{ + transaction_hash: transaction.hash, + gas_price: transaction.gas_price, + gas_used: transaction.gas_used, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + block_timestamp: transaction.block_timestamp, + block_consensus: transaction.block_consensus + } + ) - query_with_consensus = - if Map.get(filter, :allow_non_consensus) do - block_transaction_query - else - from([_, block] in block_transaction_query, - where: block.consensus == true + query_with_consensus = + if Map.get(filter, :allow_non_consensus) do + block_transaction_query + else + from([transaction] in block_transaction_query, + where: transaction.block_consensus == true + ) + end + + query_with_block_transaction_data = + from(log in logs_query, + join: block_transaction_data in subquery(query_with_consensus), + on: block_transaction_data.transaction_hash == log.transaction_hash, + order_by: block_transaction_data.block_number, + limit: 1000, + select: block_transaction_data, + select_merge: map(log, ^@log_fields) ) - end - - query_with_block_transaction_data = - from(log in logs_query, - join: block_transaction_data in subquery(query_with_consensus), - on: block_transaction_data.transaction_hash == log.transaction_hash, - order_by: block_transaction_data.block_number, - limit: 1000, - select: block_transaction_data, - select_merge: map(log, ^@log_fields) - ) - query_with_block_transaction_data - |> order_by([log], asc: log.index) - |> page_logs(paging_options) - |> Repo.replica().all() + query_with_block_transaction_data + |> order_by([log], asc: log.index) + |> page_logs(paging_options) + |> Repo.replica().all() + else + block_transaction_query = + from(transaction in Transaction, + join: block in assoc(transaction, :block), + where: block.number >= ^prepared_filter.from_block, + where: block.number <= ^prepared_filter.to_block, + select: %{ + transaction_hash: transaction.hash, + gas_price: transaction.gas_price, + gas_used: transaction.gas_used, + transaction_index: transaction.index, + block_hash: block.hash, + block_number: block.number, + block_timestamp: block.timestamp, + block_consensus: block.consensus + } + ) + + query_with_consensus = + if Map.get(filter, :allow_non_consensus) do + block_transaction_query + else + from([_, block] in block_transaction_query, + where: block.consensus == true + ) + end + + query_with_block_transaction_data = + from(log in logs_query, + join: block_transaction_data in subquery(query_with_consensus), + on: block_transaction_data.transaction_hash == log.transaction_hash, + order_by: block_transaction_data.block_number, + limit: 1000, + select: block_transaction_data, + select_merge: map(log, ^@log_fields) + ) + + query_with_block_transaction_data + |> order_by([log], asc: log.index) + |> page_logs(paging_options) + |> Repo.replica().all() + end end @topics [ diff --git a/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex b/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex index 93155d93b851..b3497d8b4c75 100644 --- a/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex +++ b/apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex @@ -10,6 +10,7 @@ defmodule Explorer.ExchangeRates do require Logger alias Explorer.Chain.Events.Publisher + alias Explorer.Market alias Explorer.ExchangeRates.{Source, Token} @interval Application.compile_env(:explorer, __MODULE__)[:cache_period] @@ -84,9 +85,11 @@ defmodule Explorer.ExchangeRates do @doc """ Lists exchange rates for the tracked tickers. """ - @spec list :: [Token.t()] + @spec list :: [Token.t()] | nil def list do - list_from_store(store()) + if enabled?() do + list_from_store(store()) + end end @doc """ @@ -121,10 +124,29 @@ defmodule Explorer.ExchangeRates do @spec fetch_rates :: Task.t() defp fetch_rates do Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fn -> - Source.fetch_exchange_rates() + case Source.fetch_exchange_rates() do + {:ok, tokens} -> {:ok, add_coin_info_from_db(tokens)} + err -> err + end end) end + defp add_coin_info_from_db(tokens) do + case Market.fetch_recent_history() do + [today | _the_rest] -> + tvl_from_history = Map.get(today, :tvl) + + tokens + |> Enum.map(fn + %Token{tvl_usd: nil} = token -> %{token | tvl_usd: tvl_from_history} + token -> token + end) + + _ -> + tokens + end + end + defp list_from_store(:ets) do table_name() |> :ets.tab2list() diff --git a/apps/explorer/lib/explorer/exchange_rates/source.ex b/apps/explorer/lib/explorer/exchange_rates/source.ex index befa6560d068..0c7a0929651f 100644 --- a/apps/explorer/lib/explorer/exchange_rates/source.ex +++ b/apps/explorer/lib/explorer/exchange_rates/source.ex @@ -6,6 +6,7 @@ defmodule Explorer.ExchangeRates.Source do alias Explorer.Chain.Hash alias Explorer.ExchangeRates.Source.CoinGecko alias Explorer.ExchangeRates.Token + alias Explorer.Helper alias HTTPoison.{Error, Response} @doc """ @@ -91,12 +92,6 @@ defmodule Explorer.ExchangeRates.Source do [{"Content-Type", "application/json"}] end - def decode_json(data) do - Jason.decode!(data) - rescue - _ -> data - end - def to_decimal(nil), do: nil def to_decimal(%Decimal{} = value), do: value @@ -125,7 +120,7 @@ defmodule Explorer.ExchangeRates.Source do parse_http_success_response(body) {:ok, %Response{body: body, status_code: status_code}} when status_code in 400..526 -> - parse_http_error_response(body) + parse_http_error_response(body, status_code) {:ok, %Response{status_code: status_code}} when status_code in 300..308 -> {:error, "Source redirected"} @@ -135,37 +130,22 @@ defmodule Explorer.ExchangeRates.Source do {:error, %Error{reason: reason}} -> {:error, reason} - - {:error, :nxdomain} -> - {:error, "Source is not responsive"} - - {:error, _} -> - {:error, "Source unknown response"} end end defp parse_http_success_response(body) do - body_json = decode_json(body) + body_json = Helper.decode_json(body) - cond do - is_map(body_json) -> - {:ok, body_json} - - is_list(body_json) -> - {:ok, body_json} - - true -> - {:ok, body} - end + {:ok, body_json} end - defp parse_http_error_response(body) do - body_json = decode_json(body) + defp parse_http_error_response(body, status_code) do + body_json = Helper.decode_json(body) if is_map(body_json) do - {:error, body_json["error"]} + {:error, "#{status_code}: #{body_json["error"]}"} else - {:error, body} + {:error, "#{status_code}: #{body}"} end end end diff --git a/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex b/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex index 1499e20e834b..40076e6f6364 100644 --- a/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex +++ b/apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex @@ -3,7 +3,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do Adapter for fetching exchange rates from https://coingecko.com """ - alias Explorer.Chain + alias Explorer.{Chain, Helper} alias Explorer.ExchangeRates.{Source, Token} import Source, only: [to_decimal: 1] @@ -13,9 +13,11 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do @impl Source def format_data(%{"market_data" => _} = json_data) do market_data = json_data["market_data"] + image_data = json_data["image"] last_updated = get_last_updated(market_data) current_price = get_current_price(market_data) + image_url = get_coin_image(image_data) id = json_data["id"] @@ -39,7 +41,8 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do name: json_data["name"], symbol: String.upcase(json_data["symbol"]), usd_value: current_price, - volume_24h_usd: to_decimal(total_volume_data_usd) + volume_24h_usd: to_decimal(total_volume_data_usd), + image_url: image_url } ] end @@ -48,6 +51,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do def format_data(%{} = market_data_for_tokens) do currency = currency() market_cap = currency <> "_market_cap" + volume_24h = currency <> "_24h_vol" market_data_for_tokens |> Enum.reduce(%{}, fn @@ -57,7 +61,8 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do acc |> Map.put(address_hash, %{ fiat_value: Map.get(market_data, currency), - circulating_market_cap: Map.get(market_data, market_cap) + circulating_market_cap: Map.get(market_data, market_cap), + volume_24h: Map.get(market_data, volume_24h) }) _ -> @@ -89,14 +94,22 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do @impl Source def format_data(_), do: [] - @spec history_url(non_neg_integer()) :: String.t() - def history_url(previous_days) do + @spec history_url(non_neg_integer(), boolean()) :: String.t() + def history_url(previous_days, secondary_coin? \\ false) do query_params = %{ "days" => previous_days, "vs_currency" => "usd" } - "#{source_url()}/market_chart?#{URI.encode_query(query_params)}" + source_url = if secondary_coin?, do: secondary_source_url(), else: source_url() + + "#{source_url}/market_chart?#{URI.encode_query(query_params)}" + end + + def secondary_source_url do + id = config(:secondary_coin_id) + + if id, do: "#{base_url()}/coins/#{id}", else: nil end @impl Source @@ -128,7 +141,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do def source_url(token_addresses) when is_list(token_addresses) do joined_addresses = token_addresses |> Enum.map_join(",", &to_string/1) - "#{base_url()}/simple/token_price/#{platform()}?vs_currencies=#{currency()}&include_market_cap=true&contract_addresses=#{joined_addresses}" + "#{base_url()}/simple/token_price/#{platform()}?vs_currencies=#{currency()}&include_market_cap=true&include_24hr_vol=true&contract_addresses=#{joined_addresses}" end @impl Source @@ -241,6 +254,20 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do end end + defp get_coin_image(image_data) do + image_url_raw = + if image_data do + image_data["thumb"] || image_data["small"] + else + nil + end + + case Helper.validate_url(image_url_raw) do + {:ok, url} -> url + _ -> nil + end + end + defp get_btc_value(id, market_data) do case get_btc_price() do {:ok, price} -> diff --git a/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex b/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex index 3f13d25ba464..739d1c0425fc 100644 --- a/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex +++ b/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex @@ -44,7 +44,8 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do name: token_properties["name"], symbol: String.upcase(token_properties["symbol"]), usd_value: current_price, - volume_24h_usd: to_decimal(total_volume_data_usd) + volume_24h_usd: to_decimal(total_volume_data_usd), + image_url: nil } ] end @@ -70,6 +71,12 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do end end + @impl Source + def source_url(:secondary_coin) do + coin_id = config(:secondary_coin_id) + if coin_id, do: "#{api_quotes_latest_url()}?id=#{coin_id}&CMC_PRO_API_KEY=#{api_key()}", else: nil + end + @impl Source def source_url(input) do case Chain.Hash.Address.cast(input) do diff --git a/apps/explorer/lib/explorer/exchange_rates/token.ex b/apps/explorer/lib/explorer/exchange_rates/token.ex index 07405701e8cc..df262bc8c9ce 100644 --- a/apps/explorer/lib/explorer/exchange_rates/token.ex +++ b/apps/explorer/lib/explorer/exchange_rates/token.ex @@ -17,6 +17,7 @@ defmodule Explorer.ExchangeRates.Token do * `:symbol` - Trading symbol used to represent a currency * `:usd_value` - The USD value of the currency * `:volume_24h_usd` - The volume from the last 24 hours in USD + * `:image_url` - Token image URL """ @type t :: %__MODULE__{ available_supply: Decimal.t() | nil, @@ -29,12 +30,13 @@ defmodule Explorer.ExchangeRates.Token do name: String.t() | nil, symbol: String.t() | nil, usd_value: Decimal.t() | nil, - volume_24h_usd: Decimal.t() | nil + volume_24h_usd: Decimal.t() | nil, + image_url: String.t() | nil } @derive Jason.Encoder - @enforce_keys ~w(available_supply total_supply btc_value id last_updated market_cap_usd tvl_usd name symbol usd_value volume_24h_usd)a - defstruct ~w(available_supply total_supply btc_value id last_updated market_cap_usd tvl_usd name symbol usd_value volume_24h_usd)a + @enforce_keys ~w(available_supply total_supply btc_value id last_updated market_cap_usd tvl_usd name symbol usd_value volume_24h_usd image_url)a + defstruct ~w(available_supply total_supply btc_value id last_updated market_cap_usd tvl_usd name symbol usd_value volume_24h_usd image_url)a def null, do: %__MODULE__{ @@ -48,6 +50,7 @@ defmodule Explorer.ExchangeRates.Token do market_cap_usd: nil, tvl_usd: nil, btc_value: nil, + image_url: nil, last_updated: nil } @@ -64,16 +67,17 @@ defmodule Explorer.ExchangeRates.Token do market_cap_usd: market_cap_usd, tvl_usd: tvl_usd, btc_value: btc_value, + image_url: image_url, last_updated: last_updated }) do # symbol is first because it is the key used for lookup in `Explorer.ExchangeRates`'s ETS table {symbol, id, name, available_supply, total_supply, usd_value, volume_24h_usd, market_cap_usd, tvl_usd, btc_value, - last_updated} + image_url, last_updated} end def from_tuple( {symbol, id, name, available_supply, total_supply, usd_value, volume_24h_usd, market_cap_usd, tvl_usd, - btc_value, last_updated} + btc_value, image_url, last_updated} ) do %__MODULE__{ symbol: symbol, @@ -86,6 +90,7 @@ defmodule Explorer.ExchangeRates.Token do market_cap_usd: market_cap_usd, tvl_usd: tvl_usd, btc_value: btc_value, + image_url: image_url, last_updated: last_updated } end diff --git a/apps/explorer/lib/explorer/graphql.ex b/apps/explorer/lib/explorer/graphql.ex index aa341e095f9a..a9d0b25ca0bf 100644 --- a/apps/explorer/lib/explorer/graphql.ex +++ b/apps/explorer/lib/explorer/graphql.ex @@ -24,7 +24,7 @@ defmodule Explorer.GraphQL do Returns a query to fetch transactions with a matching `to_address_hash`, `from_address_hash`, or `created_contract_address_hash` field for a given address hash. - Orders transactions by `block_number` and `index` according to to `order` + Orders transactions by `block_number` and `index` according to `order` """ @spec address_to_transactions_query(Hash.Address.t(), :desc | :asc) :: Ecto.Query.t() def address_to_transactions_query(address_hash, order) do diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 4b1dab59a392..0b913c6556a6 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -1,11 +1,14 @@ defmodule Explorer.Helper do @moduledoc """ - Common explorer helper + Auxiliary common functions. """ alias ABI.TypeDecoder + alias Explorer.Chain alias Explorer.Chain.Data + import Ecto.Query, only: [where: 3] + @spec decode_data(binary() | map(), list()) :: list() | nil def decode_data("0x", types) do for _ <- types, do: nil @@ -27,13 +30,118 @@ defmodule Explorer.Helper do |> TypeDecoder.decode_raw(types) end - @spec parse_integer(binary() | nil) :: integer() | nil - def parse_integer(nil), do: nil - - def parse_integer(string) do - case Integer.parse(string) do - {number, ""} -> number + def parse_integer(integer_string) when is_binary(integer_string) do + case Integer.parse(integer_string) do + {integer, ""} -> integer _ -> nil end end + + def parse_integer(value) when is_integer(value) do + value + end + + def parse_integer(_integer_string), do: nil + + @doc """ + Parses number from hex string or decimal number string + """ + @spec parse_number(binary() | nil) :: integer() | nil + def parse_number(nil), do: nil + + def parse_number(number) when is_integer(number) do + number + end + + def parse_number("0x" <> hex_number) do + {number, ""} = Integer.parse(hex_number, 16) + + number + end + + def parse_number(""), do: 0 + + def parse_number(string_number) do + {number, ""} = Integer.parse(string_number, 10) + + number + end + + @doc """ + Function to preload a `struct` for each element of the `list`. + You should specify a primary key for a `struct` in `references_field`, + and the list element's foreign key in `foreign_key_field`. + Results will be placed to `preload_field` + """ + @spec custom_preload(list(map()), keyword(), atom(), atom(), atom(), atom()) :: list() + def custom_preload(list, options, struct, foreign_key_field, references_field, preload_field) do + to_fetch_from_db = list |> Enum.map(& &1[foreign_key_field]) |> Enum.uniq() + + associated_elements = + struct + |> where([t], field(t, ^references_field) in ^to_fetch_from_db) + |> Chain.select_repo(options).all() + |> Enum.reduce(%{}, fn el, acc -> Map.put(acc, Map.from_struct(el)[references_field], el) end) + + Enum.map(list, fn el -> Map.put(el, preload_field, associated_elements[el[foreign_key_field]]) end) + end + + @doc """ + Decode json + """ + @spec decode_json(any()) :: map() | list() | nil + def decode_json(nil), do: nil + + def decode_json(data) do + if String.valid?(data) do + safe_decode_json(data) + else + data + |> :unicode.characters_to_binary(:latin1) + |> safe_decode_json() + end + end + + defp safe_decode_json(data) do + case Jason.decode(data) do + {:ok, decoded} -> decoded + _ -> %{error: data} + end + end + + @doc """ + Tries to decode binary to json, return either decoded object, or initial binary + """ + @spec maybe_decode(binary) :: any + def maybe_decode(data) do + case safe_decode_json(data) do + %{error: _} -> data + decoded -> decoded + end + end + + @doc """ + Checks if input is a valid URL + """ + @spec validate_url(String.t() | nil) :: {:ok, String.t()} | :error + def validate_url(url) when is_binary(url) do + case URI.parse(url) do + %URI{host: nil} -> :error + _ -> {:ok, url} + end + end + + def validate_url(_), do: :error + + @doc """ + Validate url + """ + @spec valid_url?(String.t()) :: boolean() + def valid_url?(string) when is_binary(string) do + uri = URI.parse(string) + + !is_nil(uri.scheme) && !is_nil(uri.host) + end + + def valid_url?(_), do: false end diff --git a/apps/explorer/lib/explorer/market/history/cataloger.ex b/apps/explorer/lib/explorer/market/history/cataloger.ex index f3c73de23d57..19b0c7bc4f1b 100644 --- a/apps/explorer/lib/explorer/market/history/cataloger.ex +++ b/apps/explorer/lib/explorer/market/history/cataloger.ex @@ -4,10 +4,9 @@ defmodule Explorer.Market.History.Cataloger do Market grabs the last 365 day's worth of market history for the configured coin in the explorer. Once that data is fetched, current day's values are - checked every 60 minutes. Additionally, failed requests to the history - source will follow exponential backoff `100ms * 2^(n+1)` where `n` is the - number of failed requests. - + checked every `MARKET_HISTORY_FETCH_INTERVAL` or every 60 minutes by default. + Additionally, failed requests to the history source will follow exponential + backoff `100ms * 2^(n+1)` where `n` is the number of failed requests. """ use GenServer @@ -41,13 +40,33 @@ defmodule Explorer.Market.History.Cataloger do @impl GenServer # Record fetch successful. - def handle_info({_ref, {:price_history, {_, _, {:ok, records}}}}, state) do - Process.send(self(), {:fetch_market_cap_history, 365}, []) + def handle_info({_ref, {:price_history, {day_count, _, false, {:ok, records}}}}, state) do + if config_or_default(:secondary_coin_enabled, false) do + Process.send(self(), {:fetch_price_history_for_secondary_coin, day_count}, []) + else + Process.send(self(), {:fetch_market_cap_history, day_count}, []) + end + state = state |> Map.put_new(:price_records, records) {:noreply, state} end + # Secondary coin. + def handle_info({_ref, {:price_history, {day_count, _, true, {:ok, records}}}}, state) do + Process.send(self(), {:fetch_market_cap_history, day_count}, []) + state = state |> Map.put_new(:secondary_coin_price_records, records) + + {:noreply, state} + end + + @impl GenServer + def handle_info({:fetch_price_history_for_secondary_coin, day_count}, state) do + fetch_price_history(day_count, true) + + {:noreply, state} + end + @impl GenServer def handle_info({:fetch_market_cap_history, day_count}, state) do fetch_market_cap_history(day_count) @@ -99,10 +118,10 @@ defmodule Explorer.Market.History.Cataloger do # Failed to get records. Try again. @impl GenServer - def handle_info({_ref, {:price_history, {day_count, failed_attempts, :error}}}, state) do + def handle_info({_ref, {:price_history, {day_count, failed_attempts, secondary_coin?, :error}}}, state) do Logger.warn(fn -> "Failed to fetch price history. Trying again." end) - fetch_price_history(day_count, failed_attempts + 1) + fetch_price_history(day_count, secondary_coin?, failed_attempts + 1) {:noreply, state} end @@ -120,7 +139,7 @@ defmodule Explorer.Market.History.Cataloger do # Failed to get records. Try again. @impl GenServer def handle_info({_ref, {:tvl_history, {day_count, failed_attempts, :error}}}, state) do - Logger.warn(fn -> "Failed to fetch market cap history. Trying again." end) + Logger.warn(fn -> "Failed to fetch tvl history. Trying again." end) fetch_tvl_history(day_count, failed_attempts + 1) @@ -151,19 +170,31 @@ defmodule Explorer.Market.History.Cataloger do Application.get_env(:explorer, __MODULE__)[key] || default end - defp market_cap_history(records, state) do + defp market_cap_history(records, _state) do Market.bulk_insert_history(records) # Schedule next check for history fetch_after = config_or_default(:history_fetch_interval, :timer.minutes(60)) Process.send_after(self(), {:fetch_price_history, 1}, fetch_after) - {:noreply, state} + {:noreply, %{}} end - @spec source_price() :: module() - defp source_price do - config_or_default(:price_source, Explorer.ExchangeRates.Source, Explorer.Market.History.Source.Price.CryptoCompare) + @spec source_price(boolean()) :: module() + defp source_price(secondary_coin?) do + if secondary_coin? do + config_or_default( + :secondary_coin_price_source, + Explorer.ExchangeRates.Source, + Explorer.Market.History.Source.Price.CryptoCompare + ) + else + config_or_default( + :price_source, + Explorer.ExchangeRates.Source, + Explorer.Market.History.Source.Price.CryptoCompare + ) + end end @spec source_market_cap() :: module() @@ -184,15 +215,17 @@ defmodule Explorer.Market.History.Cataloger do ) end - @spec fetch_price_history(non_neg_integer(), non_neg_integer()) :: Task.t() - defp fetch_price_history(day_count, failed_attempts \\ 0) do + @spec fetch_price_history(non_neg_integer(), boolean(), non_neg_integer()) :: Task.t() + defp fetch_price_history(day_count, secondary_coin? \\ false, failed_attempts \\ 0) do Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fn -> Process.sleep(HistoryProcess.delay(failed_attempts)) if failed_attempts < @price_failed_attempts do - {:price_history, {day_count, failed_attempts, source_price().fetch_price_history(day_count)}} + {:price_history, + {day_count, failed_attempts, secondary_coin?, + source_price(secondary_coin?).fetch_price_history(day_count, secondary_coin?)}} else - {:price_history, {day_count, failed_attempts, {:ok, []}}} + {:price_history, {day_count, failed_attempts, secondary_coin?, {:ok, []}}} end end) end @@ -225,13 +258,14 @@ defmodule Explorer.Market.History.Cataloger do defp compile_records(state) do price_records = state.price_records + secondary_coin_price_records = state |> Map.get(:secondary_coin_price_records, []) market_cap_records = state.market_cap_records tvl_records = state.tvl_records - all_records = price_records ++ market_cap_records ++ tvl_records + all_records = price_records ++ market_cap_records ++ tvl_records ++ secondary_coin_price_records all_records - |> Enum.group_by(fn %{date: date} -> date end) + |> Enum.group_by(fn %{date: date} = value -> {date, Map.get(value, :secondary_coin, false)} end) |> Map.values() |> Enum.map(fn a -> Enum.reduce(a, %{}, fn x, acc -> Map.merge(x, acc) end) diff --git a/apps/explorer/lib/explorer/market/history/source/price.ex b/apps/explorer/lib/explorer/market/history/source/price.ex index 07924c1fca3a..c8d697fa471a 100644 --- a/apps/explorer/lib/explorer/market/history/source/price.ex +++ b/apps/explorer/lib/explorer/market/history/source/price.ex @@ -9,11 +9,13 @@ defmodule Explorer.Market.History.Source.Price do @type record :: %{ closing_price: Decimal.t(), date: Date.t(), - opening_price: Decimal.t() + opening_price: Decimal.t(), + secondary_coin: boolean() } @doc """ Fetch history for a specified amount of days in the past. """ - @callback fetch_price_history(previous_days :: non_neg_integer()) :: {:ok, [record()]} | :error + @callback fetch_price_history(previous_days :: non_neg_integer(), secondary_coin :: boolean()) :: + {:ok, [record()]} | :error end diff --git a/apps/explorer/lib/explorer/market/history/source/price/coin_gecko.ex b/apps/explorer/lib/explorer/market/history/source/price/coin_gecko.ex index ef49d1f19cdf..8cd7ca220341 100644 --- a/apps/explorer/lib/explorer/market/history/source/price/coin_gecko.ex +++ b/apps/explorer/lib/explorer/market/history/source/price/coin_gecko.ex @@ -11,14 +11,14 @@ defmodule Explorer.Market.History.Source.Price.CoinGecko do @behaviour SourcePrice @impl SourcePrice - def fetch_price_history(previous_days) do - url = ExchangeRatesSourceCoinGecko.history_url(previous_days) + def fetch_price_history(previous_days, secondary_coin? \\ false) do + url = ExchangeRatesSourceCoinGecko.history_url(previous_days, secondary_coin?) case Source.http_request(url, ExchangeRatesSourceCoinGecko.headers()) do {:ok, data} -> result = data - |> format_data() + |> format_data(secondary_coin?) {:ok, result} @@ -27,10 +27,10 @@ defmodule Explorer.Market.History.Source.Price.CoinGecko do end end - @spec format_data(term()) :: SourcePrice.record() | nil - defp format_data(nil), do: nil + @spec format_data(term(), boolean()) :: SourcePrice.record() | nil + defp format_data(nil, _), do: nil - defp format_data(data) do + defp format_data(data, secondary_coin?) do prices = data["prices"] for [date, price] <- prices do @@ -39,7 +39,8 @@ defmodule Explorer.Market.History.Source.Price.CoinGecko do %{ closing_price: Decimal.new(to_string(price)), date: CryptoCompare.date(date), - opening_price: Decimal.new(to_string(price)) + opening_price: Decimal.new(to_string(price)), + secondary_coin: secondary_coin? } end end diff --git a/apps/explorer/lib/explorer/market/history/source/price/coin_market_cap.ex b/apps/explorer/lib/explorer/market/history/source/price/coin_market_cap.ex index 0a8c4bf28dae..c226edfcdd3b 100644 --- a/apps/explorer/lib/explorer/market/history/source/price/coin_market_cap.ex +++ b/apps/explorer/lib/explorer/market/history/source/price/coin_market_cap.ex @@ -10,15 +10,18 @@ defmodule Explorer.Market.History.Source.Price.CoinMarketCap do @behaviour SourcePrice @impl SourcePrice - def fetch_price_history(_previous_days \\ nil) do - url = ExchangeRatesSourceCoinMarketCap.source_url() + def fetch_price_history(_previous_days \\ nil, secondary_coin? \\ false) do + url = + if secondary_coin?, + do: ExchangeRatesSourceCoinMarketCap.source_url(:secondary_coin), + else: ExchangeRatesSourceCoinMarketCap.source_url() if url do case Source.http_request(url, ExchangeRatesSourceCoinMarketCap.headers()) do {:ok, data} -> result = data - |> format_data() + |> format_data(secondary_coin?) {:ok, result} @@ -30,10 +33,10 @@ defmodule Explorer.Market.History.Source.Price.CoinMarketCap do end end - @spec format_data(term()) :: SourcePrice.record() | nil - defp format_data(nil), do: nil + @spec format_data(term(), boolean()) :: SourcePrice.record() | nil + defp format_data(nil, _), do: nil - defp format_data(%{"data" => _} = json_data) do + defp format_data(%{"data" => _} = json_data, secondary_coin?) do market_data = json_data["data"] token_properties = ExchangeRatesSourceCoinMarketCap.get_token_properties(market_data) @@ -48,7 +51,8 @@ defmodule Explorer.Market.History.Source.Price.CoinMarketCap do %{ closing_price: current_price_usd, date: last_updated, - opening_price: current_price_usd + opening_price: current_price_usd, + secondary_coin: secondary_coin? } ] end diff --git a/apps/explorer/lib/explorer/market/history/source/price/crypto_compare.ex b/apps/explorer/lib/explorer/market/history/source/price/crypto_compare.ex index e1b31f03cc2f..297f72348000 100644 --- a/apps/explorer/lib/explorer/market/history/source/price/crypto_compare.ex +++ b/apps/explorer/lib/explorer/market/history/source/price/crypto_compare.ex @@ -18,15 +18,15 @@ defmodule Explorer.Market.History.Source.Price.CryptoCompare do @typep unix_timestamp :: non_neg_integer() @impl SourcePrice - def fetch_price_history(previous_days) do - url = history_url(previous_days) + def fetch_price_history(previous_days, secondary_coin?) do + url = history_url(previous_days, secondary_coin?) headers = [{"Content-Type", "application/json"}] case HTTPoison.get(url, headers) do {:ok, %Response{body: body, status_code: 200}} -> result = body - |> format_data() + |> format_data(secondary_coin?) |> reject_zeros() {:ok, result} @@ -49,23 +49,26 @@ defmodule Explorer.Market.History.Source.Price.CryptoCompare do |> DateTime.to_date() end - @spec format_data(String.t()) :: [SourcePrice.record()] - defp format_data(data) do + @spec format_data(String.t(), boolean()) :: [SourcePrice.record()] + defp format_data(data, secondary_coin?) do json = Jason.decode!(data) for item <- json["Data"] do %{ closing_price: Decimal.new(to_string(item["close"])), date: date(item["time"]), - opening_price: Decimal.new(to_string(item["open"])) + opening_price: Decimal.new(to_string(item["open"])), + secondary_coin: secondary_coin? } end end - @spec history_url(non_neg_integer()) :: String.t() - defp history_url(previous_days) do + @spec history_url(non_neg_integer(), boolean()) :: String.t() + defp history_url(previous_days, secondary_coin?) do + fsym = if secondary_coin?, do: config(:secondary_coin_symbol), else: Explorer.coin() + query_params = %{ - "fsym" => Explorer.coin(), + "fsym" => fsym, "limit" => previous_days, "tsym" => "USD" } @@ -78,4 +81,9 @@ defmodule Explorer.Market.History.Source.Price.CryptoCompare do Decimal.equal?(item.closing_price, 0) && Decimal.equal?(item.opening_price, 0) end) end + + @spec config(atom()) :: term + defp config(key) do + Application.get_env(:explorer, __MODULE__, [])[key] + end end diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex index 571f837e0fe7..11edb9bc3931 100644 --- a/apps/explorer/lib/explorer/market/market.ex +++ b/apps/explorer/lib/explorer/market/market.ex @@ -7,14 +7,16 @@ defmodule Explorer.Market do alias Explorer.Market.{MarketHistory, MarketHistoryCache} alias Explorer.{ExchangeRates, Repo} + import Ecto.Query, only: [from: 2] + @doc """ Retrieves the history for the recent specified amount of days. Today's date is include as part of the day count """ - @spec fetch_recent_history() :: [MarketHistory.t()] - def fetch_recent_history do - MarketHistoryCache.fetch() + @spec fetch_recent_history(boolean()) :: [MarketHistory.t()] + def fetch_recent_history(secondary_coin? \\ false) do + MarketHistoryCache.fetch(secondary_coin?) end @doc """ @@ -40,7 +42,8 @@ defmodule Explorer.Market do last_updated: nil, name: nil, symbol: nil, - volume_24h_usd: nil + volume_24h_usd: nil, + image_url: nil } else Token.null() @@ -50,9 +53,9 @@ defmodule Explorer.Market do @doc """ Get most recent exchange rate for the native coin from ETS or from DB. """ - @spec get_coin_exchange_rate() :: Token.t() | nil + @spec get_coin_exchange_rate() :: Token.t() def get_coin_exchange_rate do - get_exchange_rate(Explorer.coin()) || get_native_coin_exchange_rate_from_db() || Token.null() + get_native_coin_exchange_rate_from_cache() || get_native_coin_exchange_rate_from_db() || Token.null() end @doc false @@ -67,11 +70,80 @@ defmodule Explorer.Market do # Enforce MarketHistory ShareLocks order (see docs: sharelocks.md) |> Enum.sort_by(& &1.date) - Repo.insert_all(MarketHistory, records_without_zeroes, on_conflict: :nothing, conflict_target: [:date]) + Repo.insert_all(MarketHistory, records_without_zeroes, + on_conflict: market_history_on_conflict(), + conflict_target: [:date, :secondary_coin] + ) + end + + defp market_history_on_conflict do + from( + market_history in MarketHistory, + update: [ + set: [ + opening_price: + fragment( + """ + CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.opening_price IS NOT NULL AND EXCLUDED.opening_price > 0 + THEN EXCLUDED.opening_price + ELSE ? + END + """, + market_history.opening_price, + market_history.opening_price, + market_history.opening_price + ), + closing_price: + fragment( + """ + CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.closing_price IS NOT NULL AND EXCLUDED.closing_price > 0 + THEN EXCLUDED.closing_price + ELSE ? + END + """, + market_history.closing_price, + market_history.closing_price, + market_history.closing_price + ), + market_cap: + fragment( + """ + CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.market_cap IS NOT NULL AND EXCLUDED.market_cap > 0 + THEN EXCLUDED.market_cap + ELSE ? + END + """, + market_history.market_cap, + market_history.market_cap, + market_history.market_cap + ), + tvl: + fragment( + """ + CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.tvl IS NOT NULL AND EXCLUDED.tvl > 0 + THEN EXCLUDED.tvl + ELSE ? + END + """, + market_history.tvl, + market_history.tvl, + market_history.tvl + ) + ] + ], + where: + is_nil(market_history.tvl) or market_history.tvl == 0 or is_nil(market_history.market_cap) or + market_history.market_cap == 0 or is_nil(market_history.opening_price) or + market_history.opening_price == 0 or is_nil(market_history.closing_price) or + market_history.closing_price == 0 + ) end - @spec get_exchange_rate(String.t()) :: Token.t() | nil - defp get_exchange_rate(symbol) do - ExchangeRates.lookup(symbol) + @spec get_native_coin_exchange_rate_from_cache :: Token.t() | nil + defp get_native_coin_exchange_rate_from_cache do + case ExchangeRates.list() do + [native_coin] -> native_coin + _ -> nil + end end end diff --git a/apps/explorer/lib/explorer/market/market_history.ex b/apps/explorer/lib/explorer/market/market_history.ex index 0fabaf0e305c..d8ddc3ad5a33 100644 --- a/apps/explorer/lib/explorer/market/market_history.ex +++ b/apps/explorer/lib/explorer/market/market_history.ex @@ -5,14 +5,6 @@ defmodule Explorer.Market.MarketHistory do use Explorer.Schema - schema "market_history" do - field(:closing_price, :decimal) - field(:date, :date) - field(:opening_price, :decimal) - field(:market_cap, :decimal) - field(:tvl, :decimal) - end - @typedoc """ The recorded values of the configured coin to USD for a single day. @@ -20,13 +12,14 @@ defmodule Explorer.Market.MarketHistory do * `:date` - The date in UTC. * `:opening_price` - Opening price in USD. * `:market_cap` - Market cap in USD. - * `:market_cap` - TVL in USD. + * `:tvl` - TVL in USD. """ - @type t :: %__MODULE__{ - closing_price: Decimal.t(), - date: Date.t(), - opening_price: Decimal.t(), - market_cap: Decimal.t(), - tvl: Decimal.t() - } + typed_schema "market_history" do + field(:closing_price, :decimal) + field(:date, :date, null: false) + field(:opening_price, :decimal) + field(:market_cap, :decimal) + field(:tvl, :decimal) + field(:secondary_coin, :boolean) + end end diff --git a/apps/explorer/lib/explorer/market/market_history_cache.ex b/apps/explorer/lib/explorer/market/market_history_cache.ex index 0a6a9a33ab43..8d095ab35923 100644 --- a/apps/explorer/lib/explorer/market/market_history_cache.ex +++ b/apps/explorer/lib/explorer/market/market_history_cache.ex @@ -15,12 +15,15 @@ defmodule Explorer.Market.MarketHistoryCache do # 6 hours @recent_days 30 - def fetch do - if cache_expired?() do + def fetch(secondary_coin? \\ false) do + @last_update_key + |> cache_expired?() + |> if do update_cache() else fetch_from_cache(@history_key) end + |> Enum.filter(&(&1.secondary_coin == secondary_coin?)) end def cache_name, do: @cache_name @@ -31,9 +34,9 @@ defmodule Explorer.Market.MarketHistoryCache do def recent_days_count, do: @recent_days - defp cache_expired? do + defp cache_expired?(key) do cache_period = Application.get_env(:explorer, __MODULE__)[:cache_period] - updated_at = fetch_from_cache(@last_update_key) + updated_at = fetch_from_cache(key) cond do is_nil(updated_at) -> true diff --git a/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex b/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex new file mode 100644 index 000000000000..5dbbf2d94527 --- /dev/null +++ b/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex @@ -0,0 +1,203 @@ +defmodule Explorer.MicroserviceInterfaces.AccountAbstraction do + @moduledoc """ + Interface to interact with Blockscout Account Abstraction (EIP-4337) microservice + """ + + alias Explorer.Utility.Microservice + alias HTTPoison.Response + require Logger + + @doc """ + Get user operation by hash via GET {{baseUrl}}/api/v1/userOps/:hash + """ + @spec get_user_ops_by_hash(binary()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_user_ops_by_hash(user_operation_hash_string) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{} + + http_get_request(operation_by_hash_url(user_operation_hash_string), query_params) + end + end + + @doc """ + Get operations list via GET {{baseUrl}}/api/v1/operations + """ + @spec get_operations(map()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_operations(query_params) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(operations_url(), query_params) + end + end + + @doc """ + Get bundler by address hash via GET {{baseUrl}}/api/v1/bundlers/:address + """ + @spec get_bundler_by_hash(binary()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_bundler_by_hash(address_hash_string) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{} + + http_get_request(bundler_by_hash_url(address_hash_string), query_params) + end + end + + @doc """ + Get bundlers list via GET {{baseUrl}}/api/v1/bundlers + """ + @spec get_bundlers(map()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_bundlers(query_params) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(bundlers_url(), query_params) + end + end + + @doc """ + Get factory by address hash via GET {{baseUrl}}/api/v1/factories/:address + """ + @spec get_factory_by_hash(binary()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_factory_by_hash(address_hash_string) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{} + + http_get_request(factory_by_hash_url(address_hash_string), query_params) + end + end + + @doc """ + Get factories list via GET {{baseUrl}}/api/v1/factories + """ + @spec get_factories(map()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_factories(query_params) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(factories_url(), query_params) + end + end + + @doc """ + Get paymaster by address hash via GET {{baseUrl}}/api/v1/paymasters/:address + """ + @spec get_paymaster_by_hash(binary()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_paymaster_by_hash(address_hash_string) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{} + + http_get_request(paymaster_by_hash_url(address_hash_string), query_params) + end + end + + @doc """ + Get paymasters list via GET {{baseUrl}}/api/v1/paymasters + """ + @spec get_paymasters(map()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_paymasters(query_params) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(paymasters_url(), query_params) + end + end + + @doc """ + Get account by address hash via GET {{baseUrl}}/api/v1/accounts/:address + """ + @spec get_account_by_hash(binary()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_account_by_hash(address_hash_string) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{} + + http_get_request(account_by_hash_url(address_hash_string), query_params) + end + end + + @doc """ + Get accounts list via GET {{baseUrl}}/api/v1/accounts + """ + @spec get_accounts(map()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_accounts(query_params) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(accounts_url(), query_params) + end + end + + @doc """ + Get bundles list via GET {{baseUrl}}/api/v1/bundles + """ + @spec get_bundles(map()) :: {non_neg_integer(), map()} | {:error, :disabled} + def get_bundles(query_params) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(bundles_url(), query_params) + end + end + + defp http_get_request(url, query_params) do + case HTTPoison.get(url, [], params: query_params) do + {:ok, %Response{body: body, status_code: 200}} -> + {:ok, response_json} = Jason.decode(body) + {200, response_json} + + {_, %Response{body: body, status_code: status_code} = error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to Account Abstraction microservice url: #{url}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:ok, response_json} = Jason.decode(body) + {status_code, response_json} + end + end + + @spec enabled?() :: boolean + def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] + + defp operation_by_hash_url(user_op_hash) do + "#{base_url()}/userOps/#{user_op_hash}" + end + + defp operations_url do + "#{base_url()}/userOps" + end + + defp bundler_by_hash_url(address_hash) do + "#{base_url()}/bundlers/#{address_hash}" + end + + defp bundlers_url do + "#{base_url()}/bundlers" + end + + defp factory_by_hash_url(address_hash) do + "#{base_url()}/factories/#{address_hash}" + end + + defp factories_url do + "#{base_url()}/factories" + end + + defp paymaster_by_hash_url(address_hash) do + "#{base_url()}/paymasters/#{address_hash}" + end + + defp paymasters_url do + "#{base_url()}/paymasters" + end + + defp account_by_hash_url(address_hash) do + "#{base_url()}/accounts/#{address_hash}" + end + + defp accounts_url do + "#{base_url()}/accounts" + end + + defp bundles_url do + "#{base_url()}/bundles" + end + + defp base_url do + "#{Microservice.base_url(__MODULE__)}/api/v1" + end +end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex new file mode 100644 index 000000000000..251919f28900 --- /dev/null +++ b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex @@ -0,0 +1,452 @@ +defmodule Explorer.MicroserviceInterfaces.BENS do + @moduledoc """ + Interface to interact with Blockscout ENS microservice + """ + + alias Ecto.Association.NotLoaded + alias Explorer.Chain + + alias Explorer.Chain.{ + Address, + Address.CurrentTokenBalance, + Block, + InternalTransaction, + Log, + TokenTransfer, + Transaction, + Withdrawal + } + + alias Explorer.Utility.Microservice + alias HTTPoison.Response + require Logger + + @post_timeout :timer.seconds(5) + @request_error_msg "Error while sending request to BENS microservice" + + @typep supported_types :: + Address.t() + | Block.t() + | CurrentTokenBalance.t() + | InternalTransaction.t() + | Log.t() + | TokenTransfer.t() + | Transaction.t() + | Withdrawal.t() + + @doc """ + Batch request for ENS names via POST {{baseUrl}}/api/v1/:chainId/addresses:batch-resolve-names + """ + @spec ens_names_batch_request([binary()]) :: {:error, :disabled | binary() | Jason.DecodeError.t()} | {:ok, any} + def ens_names_batch_request(addresses) do + with :ok <- Microservice.check_enabled(__MODULE__) do + body = %{ + addresses: Enum.map(addresses, &to_string/1) + } + + http_post_request(batch_resolve_name_url(), body) + end + end + + @doc """ + Request for ENS name via GET {{baseUrl}}/api/v1/:chainId/addresses:lookup + """ + @spec address_lookup(binary()) :: {:error, :disabled | binary() | Jason.DecodeError.t()} | {:ok, any} + def address_lookup(address) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{ + "address" => to_string(address), + "resolved_to" => true, + "owned_by" => false, + "only_active" => true, + "order" => "ASC" + } + + http_get_request(address_lookup_url(), query_params) + end + end + + @spec get_address(binary()) :: {:error, :disabled | binary() | Jason.DecodeError.t()} | {:ok, any} + def get_address(address) do + with :ok <- Microservice.check_enabled(__MODULE__) do + http_get_request(get_address_url(address), nil) + end + end + + @doc """ + Lookup for ENS domain name via GET {{baseUrl}}/api/v1/:chainId/domains:lookup + """ + @spec ens_domain_lookup(binary()) :: {:error, :disabled | binary() | Jason.DecodeError.t()} | {:ok, any} + def ens_domain_lookup(domain) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{ + "name" => domain, + "only_active" => true, + "sort" => "registration_date", + "order" => "DESC" + } + + http_get_request(domain_lookup_url(), query_params) + end + end + + defp http_post_request(url, body) do + headers = [{"Content-Type", "application/json"}] + + case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %Response{body: body, status_code: 200}} -> + Jason.decode(body) + + {_, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to BENS microservice url: #{url}, body: #{inspect(body, limit: :infinity, printable_limit: :infinity)}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, @request_error_msg} + end + end + + defp http_get_request(url, query_params) do + case HTTPoison.get(url, [], params: query_params) do + {:ok, %Response{body: body, status_code: 200}} -> + Jason.decode(body) + + {_, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to BENS microservice url: #{url}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, @request_error_msg} + end + end + + @spec enabled?() :: boolean + def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] + + defp batch_resolve_name_url do + "#{addresses_url()}:batch-resolve-names" + end + + defp address_lookup_url do + "#{addresses_url()}:lookup" + end + + defp get_address_url(address) do + "#{addresses_url()}/#{address}" + end + + defp domain_lookup_url do + "#{domains_url()}:lookup" + end + + defp addresses_url do + "#{base_url()}/addresses" + end + + defp domains_url do + "#{base_url()}/domains" + end + + defp base_url do + chain_id = Application.get_env(:block_scout_web, :chain_id) + "#{Microservice.base_url(__MODULE__)}/api/v1/#{chain_id}" + end + + @doc """ + Preload ENS info to list of entities if enabled?() + """ + @spec maybe_preload_ens([supported_types] | supported_types) :: [supported_types] | supported_types + def maybe_preload_ens(argument, function \\ &preload_ens_to_list/1) do + if enabled?() do + function.(argument) + else + argument + end + end + + @spec maybe_preload_ens_info_to_search_results(list()) :: list() + def maybe_preload_ens_info_to_search_results(list) do + maybe_preload_ens(list, &preload_ens_info_to_search_results/1) + end + + @spec maybe_preload_ens_to_transaction(Transaction.t()) :: Transaction.t() + def maybe_preload_ens_to_transaction(transaction) do + maybe_preload_ens(transaction, &preload_ens_to_transaction/1) + end + + @spec preload_ens_to_transaction(Transaction.t()) :: Transaction.t() + def preload_ens_to_transaction(transaction) do + [transaction_with_ens] = preload_ens_to_list([transaction]) + transaction_with_ens + end + + @spec maybe_preload_ens_to_address(Address.t()) :: Address.t() + def maybe_preload_ens_to_address(address) do + maybe_preload_ens(address, &preload_ens_to_address/1) + end + + @spec preload_ens_to_address(Address.t()) :: Address.t() + def preload_ens_to_address(address) do + [address_with_ens] = preload_ens_to_list([address]) + address_with_ens + end + + @doc """ + Preload ENS names to list of entities + """ + @spec preload_ens_to_list([supported_types]) :: [supported_types] + def preload_ens_to_list(items) do + address_hash_strings = + Enum.reduce(items, [], fn item, acc -> + item_to_address_hash_strings(item) ++ acc + end) + + case ens_names_batch_request(address_hash_strings) do + {:ok, result} -> + put_ens_names(result["names"], items) + + _ -> + items + end + end + + @doc """ + Preload ENS info to search result, using get_address/1 + """ + @spec preload_ens_info_to_search_results(list) :: list + def preload_ens_info_to_search_results(list) do + Enum.map(list, fn + %{type: "address", ens_info: ens_info} = search_result when not is_nil(ens_info) -> + search_result + + %{type: "address"} = search_result -> + ens_info = search_result[:address_hash] |> get_address() |> parse_get_address_response() + Map.put(search_result, :ens_info, ens_info) + + search_result -> + search_result + end) + end + + @spec ens_domain_name_lookup(binary()) :: + nil | %{address_hash: binary(), expiry_date: any(), name: any(), names_count: integer()} + def ens_domain_name_lookup(domain) do + domain |> ens_domain_lookup() |> parse_lookup_response() + end + + defp parse_lookup_response( + {:ok, + %{ + "items" => + [ + %{"name" => name, "expiry_date" => expiry_date, "resolved_address" => %{"hash" => address_hash_string}} + | _other + ] = items + }} + ) do + {:ok, hash} = Chain.string_to_address_hash(address_hash_string) + + %{ + name: name, + expiry_date: expiry_date, + names_count: Enum.count(items), + address_hash: Address.checksum(hash) + } + end + + defp parse_lookup_response(_), do: nil + + defp parse_get_address_response( + {:ok, + %{ + "domain" => %{ + "name" => name, + "expiry_date" => expiry_date, + "resolved_address" => %{"hash" => address_hash_string} + }, + "resolved_domains_count" => resolved_domains_count + }} + ) do + {:ok, hash} = Chain.string_to_address_hash(address_hash_string) + + %{ + name: name, + expiry_date: expiry_date, + names_count: resolved_domains_count, + address_hash: Address.checksum(hash) + } + end + + defp parse_get_address_response(_), do: nil + + defp item_to_address_hash_strings(%Transaction{ + to_address_hash: to_address_hash, + created_contract_address_hash: created_contract_address_hash, + from_address_hash: from_address_hash, + token_transfers: token_transfers + }) do + token_transfers_addresses = + case token_transfers do + token_transfers_list when is_list(token_transfers_list) -> + List.flatten(Enum.map(token_transfers_list, &item_to_address_hash_strings/1)) + + _ -> + [] + end + + ([to_address_hash, created_contract_address_hash, from_address_hash] + |> Enum.reject(&is_nil/1) + |> Enum.map(&to_string/1)) ++ token_transfers_addresses + end + + defp item_to_address_hash_strings(%TokenTransfer{ + to_address_hash: to_address_hash, + from_address_hash: from_address_hash + }) do + [to_string(to_address_hash), to_string(from_address_hash)] + end + + defp item_to_address_hash_strings(%InternalTransaction{ + to_address_hash: to_address_hash, + from_address_hash: from_address_hash + }) do + [to_string(to_address_hash), to_string(from_address_hash)] + end + + defp item_to_address_hash_strings(%Log{address_hash: address_hash}) do + [to_string(address_hash)] + end + + defp item_to_address_hash_strings(%Withdrawal{address_hash: address_hash}) do + [to_string(address_hash)] + end + + defp item_to_address_hash_strings(%Block{miner_hash: miner_hash}) do + [to_string(miner_hash)] + end + + defp item_to_address_hash_strings(%CurrentTokenBalance{address_hash: address_hash}) do + [to_string(address_hash)] + end + + defp item_to_address_hash_strings({%Address{} = address, _}) do + item_to_address_hash_strings(address) + end + + defp item_to_address_hash_strings(%Address{hash: hash}) do + [to_string(hash)] + end + + defp put_ens_names(names, items) do + Enum.map(items, &put_ens_name_to_item(&1, names)) + end + + defp put_ens_name_to_item( + %Transaction{ + to_address_hash: to_address_hash, + created_contract_address_hash: created_contract_address_hash, + from_address_hash: from_address_hash + } = tx, + names + ) do + token_transfers = + case tx.token_transfers do + token_transfers_list when is_list(token_transfers_list) -> + Enum.map(token_transfers_list, &put_ens_name_to_item(&1, names)) + + other -> + other + end + + %Transaction{ + tx + | to_address: alter_address(tx.to_address, to_address_hash, names), + created_contract_address: alter_address(tx.created_contract_address, created_contract_address_hash, names), + from_address: alter_address(tx.from_address, from_address_hash, names), + token_transfers: token_transfers + } + end + + defp put_ens_name_to_item( + %TokenTransfer{ + to_address_hash: to_address_hash, + from_address_hash: from_address_hash + } = tt, + names + ) do + %TokenTransfer{ + tt + | to_address: alter_address(tt.to_address, to_address_hash, names), + from_address: alter_address(tt.from_address, from_address_hash, names) + } + end + + defp put_ens_name_to_item( + %InternalTransaction{ + to_address_hash: to_address_hash, + created_contract_address_hash: created_contract_address_hash, + from_address_hash: from_address_hash + } = tx, + names + ) do + %InternalTransaction{ + tx + | to_address: alter_address(tx.to_address, to_address_hash, names), + created_contract_address: alter_address(tx.created_contract_address, created_contract_address_hash, names), + from_address: alter_address(tx.from_address, from_address_hash, names) + } + end + + defp put_ens_name_to_item(%Log{address_hash: address_hash} = log, names) do + %Log{log | address: alter_address(log.address, address_hash, names)} + end + + defp put_ens_name_to_item(%Withdrawal{address_hash: address_hash} = withdrawal, names) do + %Withdrawal{withdrawal | address: alter_address(withdrawal.address, address_hash, names)} + end + + defp put_ens_name_to_item(%Block{miner_hash: miner_hash} = block, names) do + %Block{block | miner: alter_address(block.miner, miner_hash, names)} + end + + defp put_ens_name_to_item(%CurrentTokenBalance{address_hash: address_hash} = current_token_balance, names) do + %CurrentTokenBalance{ + current_token_balance + | address: alter_address(current_token_balance.address, address_hash, names) + } + end + + defp put_ens_name_to_item({%Address{} = address, count}, names) do + {put_ens_name_to_item(address, names), count} + end + + defp put_ens_name_to_item(%Address{} = address, names) do + alter_address(address, address.hash, names) + end + + defp alter_address(_, nil, _names) do + nil + end + + defp alter_address(%NotLoaded{}, address_hash, names) do + %{ens_domain_name: names[to_string(address_hash)]} + end + + defp alter_address(%Address{} = address, address_hash, names) do + %Address{address | ens_domain_name: names[to_string(address_hash)]} + end +end diff --git a/apps/explorer/lib/explorer/migrator/address_current_token_balance_token_type.ex b/apps/explorer/lib/explorer/migrator/address_current_token_balance_token_type.ex new file mode 100644 index 000000000000..93226db73fce --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/address_current_token_balance_token_type.ex @@ -0,0 +1,51 @@ +defmodule Explorer.Migrator.AddressCurrentTokenBalanceTokenType do + @moduledoc """ + Fill empty token_type's for address_current_token_balances + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "ctb_token_type" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers do + limit = batch_size() * concurrency() + + unprocessed_data_query() + |> select([ctb], ctb.id) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + end + + @impl FillingMigration + def unprocessed_data_query do + from(ctb in CurrentTokenBalance, where: is_nil(ctb.token_type)) + end + + @impl FillingMigration + def update_batch(token_balance_ids) do + query = + from(current_token_balance in CurrentTokenBalance, + join: token in assoc(current_token_balance, :token), + where: current_token_balance.id in ^token_balance_ids, + update: [set: [token_type: token.type]] + ) + + Repo.update_all(query, [], timeout: :infinity) + end + + @impl FillingMigration + def update_cache do + BackgroundMigrations.set_ctb_token_type_finished(true) + end +end diff --git a/apps/explorer/lib/explorer/migrator/address_token_balance_token_type.ex b/apps/explorer/lib/explorer/migrator/address_token_balance_token_type.ex new file mode 100644 index 000000000000..9427db73ed60 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/address_token_balance_token_type.ex @@ -0,0 +1,51 @@ +defmodule Explorer.Migrator.AddressTokenBalanceTokenType do + @moduledoc """ + Fill empty token_type's for address_token_balances + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "tb_token_type" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers do + limit = batch_size() * concurrency() + + unprocessed_data_query() + |> select([tb], tb.id) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + end + + @impl FillingMigration + def unprocessed_data_query do + from(tb in TokenBalance, where: is_nil(tb.token_type)) + end + + @impl FillingMigration + def update_batch(token_balance_ids) do + query = + from(token_balance in TokenBalance, + join: token in assoc(token_balance, :token), + where: token_balance.id in ^token_balance_ids, + update: [set: [token_type: token.type]] + ) + + Repo.update_all(query, [], timeout: :infinity) + end + + @impl FillingMigration + def update_cache do + BackgroundMigrations.set_tb_token_type_finished(true) + end +end diff --git a/apps/explorer/lib/explorer/migrator/filling_migration.ex b/apps/explorer/lib/explorer/migrator/filling_migration.ex new file mode 100644 index 000000000000..507dfcb6e5f7 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/filling_migration.ex @@ -0,0 +1,84 @@ +defmodule Explorer.Migrator.FillingMigration do + @moduledoc """ + Template for creating migrations that fills some fields in existing entities + """ + + @callback migration_name :: String.t() + @callback unprocessed_data_query :: Ecto.Query.t() + @callback last_unprocessed_identifiers :: [any()] + @callback update_batch([any()]) :: any() + @callback update_cache :: any() + + defmacro __using__(_opts) do + quote do + @behaviour Explorer.Migrator.FillingMigration + + use GenServer, restart: :transient + + import Ecto.Query + + alias Explorer.Migrator.MigrationStatus + alias Explorer.Repo + + @default_batch_size 500 + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def migration_finished? do + MigrationStatus.get_status(migration_name()) == "completed" + end + + @impl true + def init(_) do + case MigrationStatus.get_status(migration_name()) do + "completed" -> + update_cache() + :ignore + + _ -> + MigrationStatus.set_status(migration_name(), "started") + schedule_batch_migration() + {:ok, %{}} + end + end + + @impl true + def handle_info(:migrate_batch, state) do + case last_unprocessed_identifiers() do + [] -> + update_cache() + MigrationStatus.set_status(migration_name(), "completed") + {:stop, :normal, state} + + hashes -> + hashes + |> Enum.chunk_every(batch_size()) + |> Enum.map(&run_task/1) + |> Task.await_many(:infinity) + + schedule_batch_migration() + + {:noreply, state} + end + end + + defp run_task(batch), do: Task.async(fn -> update_batch(batch) end) + + defp schedule_batch_migration do + Process.send(self(), :migrate_batch, []) + end + + defp batch_size do + Application.get_env(:explorer, __MODULE__)[:batch_size] || @default_batch_size + end + + defp concurrency do + default = 4 * System.schedulers_online() + + Application.get_env(:explorer, __MODULE__)[:concurrency] || default + end + end + end +end diff --git a/apps/explorer/lib/explorer/migrator/migration_status.ex b/apps/explorer/lib/explorer/migrator/migration_status.ex new file mode 100644 index 000000000000..e59011e64a89 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/migration_status.ex @@ -0,0 +1,32 @@ +defmodule Explorer.Migrator.MigrationStatus do + @moduledoc """ + Module is responsible for keeping the current status of background migrations. + """ + use Explorer.Schema + + alias Explorer.Repo + + @primary_key false + typed_schema "migrations_status" do + field(:migration_name, :string) + # ["started", "completed"] + field(:status, :string) + + timestamps() + end + + @doc false + def changeset(migration_status \\ %__MODULE__{}, params) do + cast(migration_status, params, [:migration_name, :status]) + end + + def get_status(migration_name) do + Repo.one(from(ms in __MODULE__, where: ms.migration_name == ^migration_name, select: ms.status)) + end + + def set_status(migration_name, status) do + %{migration_name: migration_name, status: status} + |> changeset() + |> Repo.insert(on_conflict: {:replace_all_except, [:inserted_at]}, conflict_target: :migration_name) + end +end diff --git a/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex b/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex new file mode 100644 index 000000000000..42f852b2e13d --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex @@ -0,0 +1,136 @@ +defmodule Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers do + @moduledoc """ + Delete all token transfers of ERC-721 tokens with deposit/withdrawal signatures + Delete all token transfers of ERC-1155 tokens with empty amount, amounts and token_ids + Send blocks containing token transfers of ERC-721 tokens with empty token_ids to re-fetch + """ + + use GenServer, restart: :transient + + import Ecto.Query + + require Logger + + alias Explorer.Chain.Import.Runner.Blocks + alias Explorer.Chain.{Log, TokenTransfer} + alias Explorer.Migrator.MigrationStatus + alias Explorer.Repo + + @migration_name "sanitize_incorrect_nft" + @default_batch_size 500 + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_) do + case MigrationStatus.get_status(@migration_name) do + "completed" -> + :ignore + + _ -> + MigrationStatus.set_status(@migration_name, "started") + schedule_batch_migration() + {:ok, %{step: :delete}} + end + end + + @impl true + def handle_info(:migrate_batch, %{step: step} = state) do + case last_unprocessed_identifiers(step) do + [] -> + case step do + :delete -> + Logger.info("SanitizeIncorrectNFTTokenTransfers deletion finished, continuing with blocks re-fetch") + schedule_batch_migration() + {:noreply, %{step: :refetch}} + + :refetch -> + Logger.info("SanitizeIncorrectNFTTokenTransfers migration finished") + MigrationStatus.set_status(@migration_name, "completed") + {:stop, :normal, state} + end + + identifiers -> + identifiers + |> Enum.chunk_every(batch_size()) + |> Enum.map(&run_task(&1, step)) + |> Task.await_many(:infinity) + + schedule_batch_migration() + + {:noreply, state} + end + end + + defp last_unprocessed_identifiers(step) do + limit = batch_size() * concurrency() + + step + |> unprocessed_identifiers() + |> limit(^limit) + |> Repo.all(timeout: :infinity) + end + + defp unprocessed_identifiers(:delete) do + from( + tt in TokenTransfer, + left_join: l in Log, + on: tt.block_hash == l.block_hash and tt.transaction_hash == l.transaction_hash and tt.log_index == l.index, + left_join: t in assoc(tt, :token), + where: + t.type == ^"ERC-721" and + (l.first_topic == ^TokenTransfer.weth_deposit_signature() or + l.first_topic == ^TokenTransfer.weth_withdrawal_signature()), + or_where: t.type == ^"ERC-1155" and is_nil(tt.amount) and is_nil(tt.amounts) and is_nil(tt.token_ids), + select: {tt.transaction_hash, tt.block_hash, tt.log_index} + ) + end + + defp unprocessed_identifiers(:refetch) do + from( + tt in TokenTransfer, + join: t in assoc(tt, :token), + join: b in assoc(tt, :block), + where: t.type == ^"ERC-721" and is_nil(tt.token_ids), + where: b.consensus == true, + select: tt.block_number, + distinct: tt.block_number + ) + end + + defp run_task(batch, step), do: Task.async(fn -> handle_batch(batch, step) end) + + defp handle_batch(token_transfer_ids, :delete) do + token_transfer_ids + |> build_delete_query() + |> Repo.query!([], timeout: :infinity) + end + + defp handle_batch(block_numbers, :refetch) do + Blocks.invalidate_consensus_blocks(block_numbers) + end + + defp schedule_batch_migration do + Process.send(self(), :migrate_batch, []) + end + + defp batch_size do + Application.get_env(:explorer, __MODULE__)[:batch_size] || @default_batch_size + end + + defp concurrency do + default = 4 * System.schedulers_online() + + Application.get_env(:explorer, __MODULE__)[:concurrency] || default + end + + defp build_delete_query(token_transfer_ids) do + """ + DELETE + FROM token_transfers tt + WHERE (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{TokenTransfer.encode_token_transfer_ids(token_transfer_ids)} + """ + end +end diff --git a/apps/explorer/lib/explorer/migrator/sanitize_missing_block_ranges.ex b/apps/explorer/lib/explorer/migrator/sanitize_missing_block_ranges.ex new file mode 100644 index 000000000000..29408229c021 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/sanitize_missing_block_ranges.ex @@ -0,0 +1,34 @@ +defmodule Explorer.Migrator.SanitizeMissingBlockRanges do + @moduledoc """ + Remove invalid missing block ranges (from_number < to_number and intersecting ones) + """ + + use GenServer, restart: :transient + + alias Explorer.Migrator.MigrationStatus + alias Explorer.Utility.MissingBlockRange + + @migration_name "sanitize_missing_ranges" + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(_) do + case MigrationStatus.get_status(@migration_name) do + "completed" -> + :ignore + + _ -> + MigrationStatus.set_status(@migration_name, "started") + {:ok, %{}, {:continue, :ok}} + end + end + + def handle_continue(:ok, state) do + MissingBlockRange.sanitize_missing_block_ranges() + MigrationStatus.set_status(@migration_name, "completed") + + {:stop, :normal, state} + end +end diff --git a/apps/explorer/lib/explorer/migrator/token_transfer_token_type.ex b/apps/explorer/lib/explorer/migrator/token_transfer_token_type.ex new file mode 100644 index 000000000000..bb6ee477ab96 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/token_transfer_token_type.ex @@ -0,0 +1,60 @@ +defmodule Explorer.Migrator.TokenTransferTokenType do + @moduledoc """ + Migrates all token_transfers to have set token_type + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.TokenTransfer + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "tt_denormalization" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers do + limit = batch_size() * concurrency() + + unprocessed_data_query() + |> select([tt], {tt.transaction_hash, tt.block_hash, tt.log_index}) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + end + + @impl FillingMigration + def unprocessed_data_query do + from(tt in TokenTransfer, where: is_nil(tt.token_type)) + end + + @impl FillingMigration + def update_batch(token_transfer_ids) do + token_transfer_ids + |> build_update_query() + |> Repo.query!([], timeout: :infinity) + end + + @impl FillingMigration + def update_cache do + BackgroundMigrations.set_tt_denormalization_finished(true) + end + + defp build_update_query(token_transfer_ids) do + """ + UPDATE token_transfers tt + SET token_type = CASE WHEN t.type = 'ERC-1155' AND token_ids IS NULL THEN 'ERC-20' + ELSE t.type + END, + block_consensus = b.consensus + FROM tokens t, blocks b + WHERE tt.block_hash = b.hash + AND tt.token_contract_address_hash = t.contract_address_hash + AND (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{TokenTransfer.encode_token_transfer_ids(token_transfer_ids)}; + """ + end +end diff --git a/apps/explorer/lib/explorer/migrator/transactions_denormalization.ex b/apps/explorer/lib/explorer/migrator/transactions_denormalization.ex new file mode 100644 index 000000000000..4e8493ad0a0c --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/transactions_denormalization.ex @@ -0,0 +1,53 @@ +defmodule Explorer.Migrator.TransactionsDenormalization do + @moduledoc """ + Migrates all transactions to have set block_consensus and block_timestamp + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.Transaction + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "denormalization" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers do + limit = batch_size() * concurrency() + + unprocessed_data_query() + |> select([t], t.hash) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + end + + @impl FillingMigration + def unprocessed_data_query do + from(t in Transaction, + where: not is_nil(t.block_hash) and (is_nil(t.block_consensus) or is_nil(t.block_timestamp)) + ) + end + + @impl FillingMigration + def update_batch(transaction_hashes) do + query = + from(transaction in Transaction, + join: block in assoc(transaction, :block), + where: transaction.hash in ^transaction_hashes, + update: [set: [block_consensus: block.consensus, block_timestamp: block.timestamp]] + ) + + Repo.update_all(query, [], timeout: :infinity) + end + + @impl FillingMigration + def update_cache do + BackgroundMigrations.set_transactions_denormalization_finished(true) + end +end diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex index c4d0c8f3f489..a1d4f35ad76d 100644 --- a/apps/explorer/lib/explorer/repo.ex +++ b/apps/explorer/lib/explorer/repo.ex @@ -137,21 +137,7 @@ defmodule Explorer.Repo do read_only: true def init(_, opts) do - db_url = Application.get_env(:explorer, Explorer.Repo.Replica1)[:url] - repo_conf = Application.get_env(:explorer, Explorer.Repo.Replica1) - - merged = - %{url: db_url} - |> ConfigHelper.get_db_config() - |> Keyword.merge(repo_conf, fn - _key, v1, nil -> v1 - _key, nil, v2 -> v2 - _, _, v2 -> v2 - end) - - Application.put_env(:explorer, Explorer.Repo.Replica1, merged) - - {:ok, Keyword.put(opts, :url, db_url)} + ConfigHelper.init_repo_module(__MODULE__, opts) end end @@ -161,21 +147,17 @@ defmodule Explorer.Repo do adapter: Ecto.Adapters.Postgres def init(_, opts) do - db_url = Application.get_env(:explorer, Explorer.Repo.Account)[:url] - repo_conf = Application.get_env(:explorer, Explorer.Repo.Account) - - merged = - %{url: db_url} - |> ConfigHelper.get_db_config() - |> Keyword.merge(repo_conf, fn - _key, v1, nil -> v1 - _key, nil, v2 -> v2 - _, _, v2 -> v2 - end) + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end - Application.put_env(:explorer, Explorer.Repo.Account, merged) + defmodule Optimism do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres - {:ok, Keyword.put(opts, :url, db_url)} + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) end end @@ -185,25 +167,21 @@ defmodule Explorer.Repo do adapter: Ecto.Adapters.Postgres def init(_, opts) do - db_url = Application.get_env(:explorer, Explorer.Repo.PolygonEdge)[:url] - repo_conf = Application.get_env(:explorer, Explorer.Repo.PolygonEdge) - - merged = - %{url: db_url} - |> ConfigHelper.get_db_config() - |> Keyword.merge(repo_conf, fn - _key, v1, nil -> v1 - _key, nil, v2 -> v2 - _, _, v2 -> v2 - end) + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end - Application.put_env(:explorer, Explorer.Repo.PolygonEdge, merged) + defmodule PolygonZkevm do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres - {:ok, Keyword.put(opts, :url, db_url)} + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) end end - defmodule PolygonZkevm do + defmodule ZkSync do use Ecto.Repo, otp_app: :explorer, adapter: Ecto.Adapters.Postgres @@ -233,21 +211,17 @@ defmodule Explorer.Repo do adapter: Ecto.Adapters.Postgres def init(_, opts) do - db_url = Application.get_env(:explorer, __MODULE__)[:url] - repo_conf = Application.get_env(:explorer, __MODULE__) - - merged = - %{url: db_url} - |> ConfigHelper.get_db_config() - |> Keyword.merge(repo_conf, fn - _key, v1, nil -> v1 - _key, nil, v2 -> v2 - _, _, v2 -> v2 - end) + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end - Application.put_env(:explorer, __MODULE__, merged) + defmodule Shibarium do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres - {:ok, Keyword.put(opts, :url, db_url)} + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) end end @@ -257,21 +231,47 @@ defmodule Explorer.Repo do adapter: Ecto.Adapters.Postgres def init(_, opts) do - db_url = Application.get_env(:explorer, __MODULE__)[:url] - repo_conf = Application.get_env(:explorer, __MODULE__) + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end - merged = - %{url: db_url} - |> ConfigHelper.get_db_config() - |> Keyword.merge(repo_conf, fn - _key, v1, nil -> v1 - _key, nil, v2 -> v2 - _, _, v2 -> v2 - end) + defmodule Beacon do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres - Application.put_env(:explorer, __MODULE__, merged) + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end - {:ok, Keyword.put(opts, :url, db_url)} + defmodule BridgedTokens do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres + + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end + + defmodule Filecoin do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres + + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) + end + end + + defmodule Stability do + use Ecto.Repo, + otp_app: :explorer, + adapter: Ecto.Adapters.Postgres + + def init(_, opts) do + ConfigHelper.init_repo_module(__MODULE__, opts) end end end diff --git a/apps/explorer/lib/explorer/repo/config_helper.ex b/apps/explorer/lib/explorer/repo/config_helper.ex index abfe46c958ef..e1edad2bc230 100644 --- a/apps/explorer/lib/explorer/repo/config_helper.ex +++ b/apps/explorer/lib/explorer/repo/config_helper.ex @@ -32,6 +32,24 @@ defmodule Explorer.Repo.ConfigHelper do def get_api_db_url, do: System.get_env("DATABASE_READ_ONLY_API_URL") || System.get_env("DATABASE_URL") + def init_repo_module(module, opts) do + db_url = Application.get_env(:explorer, module)[:url] + repo_conf = Application.get_env(:explorer, module) + + merged = + %{url: db_url} + |> get_db_config() + |> Keyword.merge(repo_conf, fn + _key, v1, nil -> v1 + _key, nil, v2 -> v2 + _, _, v2 -> v2 + end) + + Application.put_env(:explorer, module, merged) + + {:ok, Keyword.put(opts, :url, db_url)} + end + def ssl_enabled?, do: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true") defp extract_parameters(empty) when empty == nil or empty == "", do: [] diff --git a/apps/explorer/lib/explorer/schema.ex b/apps/explorer/lib/explorer/schema.ex index b59631550f56..ef88e49fa60d 100644 --- a/apps/explorer/lib/explorer/schema.ex +++ b/apps/explorer/lib/explorer/schema.ex @@ -3,7 +3,7 @@ defmodule Explorer.Schema do defmacro __using__(_opts) do quote do - use Ecto.Schema + use TypedEctoSchema import Ecto.{Changeset, Query} diff --git a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex index e3d2bcde8a07..3a0e898ff5e2 100644 --- a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex +++ b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex @@ -31,36 +31,25 @@ defmodule Explorer.SmartContract.CompilerVersion do end defp fetch_solc_versions do - if RustVerifierInterface.enabled?() do - RustVerifierInterface.get_versions_list() - else - headers = [{"Content-Type", "application/json"}] - - case HTTPoison.get(source_url(:solc), headers) do - {:ok, %{status_code: 200, body: body}} -> - {:ok, format_data(body, :solc)} - - {:ok, %{status_code: _status_code, body: body}} -> - {:error, decode_json(body)["error"]} - - {:error, %{reason: reason}} -> - {:error, reason} - end - end + fetch_compiler_versions(&RustVerifierInterface.get_versions_list/0, :solc) end defp fetch_vyper_versions do + fetch_compiler_versions(&RustVerifierInterface.vyper_get_versions_list/0, :vyper) + end + + defp fetch_compiler_versions(compiler_list_fn, compiler_type) do if RustVerifierInterface.enabled?() do - RustVerifierInterface.vyper_get_versions_list() + compiler_list_fn.() else headers = [{"Content-Type", "application/json"}] - case HTTPoison.get(source_url(:vyper), headers) do + case HTTPoison.get(source_url(compiler_type), headers) do {:ok, %{status_code: 200, body: body}} -> - {:ok, format_data(body, :vyper)} + {:ok, format_data(body, compiler_type)} {:ok, %{status_code: _status_code, body: body}} -> - {:error, decode_json(body)["error"]} + {:error, Helper.decode_json(body)["error"]} {:error, %{reason: reason}} -> {:error, reason} @@ -140,10 +129,6 @@ defmodule Explorer.SmartContract.CompilerVersion do end end - defp decode_json(json) do - Jason.decode!(json) - end - @spec source_url(:solc | :vyper) :: String.t() defp source_url(compiler) do case compiler do diff --git a/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex b/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex index b4ffab08328c..82b83c053c7d 100644 --- a/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex @@ -17,6 +17,15 @@ defmodule Explorer.SmartContract.EthBytecodeDBInterface do end end + @doc """ + Function to search smart contracts in eth-bytecode-db, similar to `search_contract/2` but + this function uses only `/api/v2/bytecodes/sources:search` method + """ + @spec search_contract_in_eth_bytecode_internal_db(map()) :: {:error, any} | {:ok, any} + def search_contract_in_eth_bytecode_internal_db(%{"bytecode" => _, "bytecodeType" => _} = body) do + http_post_request(bytecode_search_sources_url(), body) + end + def process_verifier_response(%{"sourcifySources" => [src | _]}) do {:ok, Map.put(src, "sourcify?", true)} end diff --git a/apps/explorer/lib/explorer/smart_contract/helper.ex b/apps/explorer/lib/explorer/smart_contract/helper.ex index c73ea123b919..27ce4413362e 100644 --- a/apps/explorer/lib/explorer/smart_contract/helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/helper.ex @@ -3,7 +3,8 @@ defmodule Explorer.SmartContract.Helper do SmartContract helper functions """ - alias Explorer.Chain + alias Explorer.{Chain, Helper} + alias Explorer.Chain.{Hash, SmartContract} alias Phoenix.HTML def queriable_method?(method) do @@ -17,12 +18,13 @@ defmodule Explorer.SmartContract.Helper do def error?(function), do: function["type"] == "error" @doc """ - Checks whether the function which is not queriable can be consider as read function or not. + Checks whether the function which is not queriable can be considered as read + function or not. """ @spec read_with_wallet_method?(%{}) :: true | false def read_with_wallet_method?(function), do: - !error?(function) && !event?(function) && !constructor?(function) && nonpayable?(function) && + !error?(function) && !event?(function) && !constructor?(function) && !empty_outputs?(function) def empty_outputs?(function), do: is_nil(function["outputs"]) || function["outputs"] == [] @@ -135,4 +137,71 @@ defmodule Explorer.SmartContract.Helper do nil end end + + @doc """ + Returns a tuple: `{creation_bytecode, deployed_bytecode, metadata}` where `metadata` is a map: + { + "blockNumber": "string", + "chainId": "string", + "contractAddress": "string", + "creationCode": "string", + "deployer": "string", + "runtimeCode": "string", + "transactionHash": "string", + "transactionIndex": "string" + } + + Metadata will be sent to a verifier microservice + """ + @spec fetch_data_for_verification(binary() | Hash.t()) :: {binary() | nil, binary(), map()} + def fetch_data_for_verification(address_hash) do + deployed_bytecode = Chain.smart_contract_bytecode(address_hash) + + metadata = %{ + "contractAddress" => to_string(address_hash), + "runtimeCode" => to_string(deployed_bytecode), + "chainId" => Application.get_env(:block_scout_web, :chain_id) + } + + case SmartContract.creation_tx_with_bytecode(address_hash) do + %{init: init, tx: tx} -> + {init, deployed_bytecode, tx |> tx_to_metadata(init) |> Map.merge(metadata)} + + %{init: init, internal_tx: internal_tx} -> + {init, deployed_bytecode, internal_tx |> internal_tx_to_metadata(init) |> Map.merge(metadata)} + + _ -> + {nil, deployed_bytecode, metadata} + end + end + + defp tx_to_metadata(tx, init) do + %{ + "blockNumber" => to_string(tx.block_number), + "transactionHash" => to_string(tx.hash), + "transactionIndex" => to_string(tx.index), + "deployer" => to_string(tx.from_address_hash), + "creationCode" => to_string(init) + } + end + + defp internal_tx_to_metadata(internal_tx, init) do + %{ + "blockNumber" => to_string(internal_tx.block_number), + "transactionHash" => to_string(internal_tx.transaction_hash), + "transactionIndex" => to_string(internal_tx.transaction_index), + "deployer" => to_string(internal_tx.from_address_hash), + "creationCode" => to_string(init) + } + end + + @doc """ + Prepare license type for verification. + """ + @spec prepare_license_type(any()) :: atom() | integer() | binary() | nil + def prepare_license_type(atom_or_integer) when is_atom(atom_or_integer) or is_integer(atom_or_integer), + do: atom_or_integer + + def prepare_license_type(binary) when is_binary(binary), do: Helper.parse_integer(binary) || binary + def prepare_license_type(_), do: nil end diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 19b8a86af152..93b80abb9c2c 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -3,12 +3,12 @@ defmodule Explorer.SmartContract.Reader do Reads Smart Contract functions from the blockchain. For information on smart contract's Application Binary Interface (ABI), visit the - [wiki](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI). + [wiki](https://docs.soliditylang.org/en/develop/abi-spec.html). """ alias EthereumJSONRPC.{Contract, Encoder} - alias Explorer.Chain alias Explorer.Chain.{Hash, SmartContract} + alias Explorer.Chain.SmartContract.Proxy alias Explorer.SmartContract.Helper @typedoc """ @@ -62,10 +62,17 @@ defmodule Explorer.SmartContract.Reader do ) # => %{"sum" => {:error, "Data overflow encoding int, data `abc` cannot fit in 256 bits"}} """ - @spec query_verified_contract(Hash.Address.t(), functions(), String.t() | nil, true | false, SmartContract.abi()) :: + @spec query_verified_contract( + Hash.Address.t(), + functions(), + String.t() | nil, + true | false, + SmartContract.abi(), + Keyword.t() + ) :: functions_results() - def query_verified_contract(address_hash, functions, from, leave_error_as_map, mabi) do - query_verified_contract_inner(address_hash, functions, mabi, from, leave_error_as_map) + def query_verified_contract(address_hash, functions, from, leave_error_as_map, mabi, options \\ []) do + query_verified_contract_inner(address_hash, functions, mabi, from, leave_error_as_map, options) end @spec query_verified_contract(Hash.Address.t(), functions(), true | false, SmartContract.abi() | nil) :: @@ -79,20 +86,21 @@ defmodule Explorer.SmartContract.Reader do functions(), SmartContract.abi() | nil, String.t() | nil, - true | false + true | false, + Keyword.t() ) :: functions_results() - defp query_verified_contract_inner(address_hash, functions, mabi, from, leave_error_as_map) do + defp query_verified_contract_inner(address_hash, functions, mabi, from, leave_error_as_map, options \\ []) do contract_address = Hash.to_string(address_hash) abi = prepare_abi(mabi, address_hash) - query_contract(contract_address, from, abi, functions, leave_error_as_map) + query_contract(contract_address, from, abi, functions, leave_error_as_map, options) end defp prepare_abi(nil, address_hash) do address_hash - |> Chain.address_hash_to_smart_contract() + |> SmartContract.address_hash_to_smart_contract() |> Map.get(:abi) end @@ -114,10 +122,11 @@ defmodule Explorer.SmartContract.Reader do String.t() | nil, term(), functions(), - true | false + true | false, + Keyword.t() ) :: functions_results() - def query_contract(contract_address, from \\ nil, abi, functions, leave_error_as_map) do - query_contract_inner(contract_address, abi, functions, nil, from, leave_error_as_map) + def query_contract(contract_address, from \\ nil, abi, functions, leave_error_as_map, options \\ []) do + query_contract_inner(contract_address, abi, functions, nil, from, leave_error_as_map, options) end @spec query_contract_by_block_number( @@ -130,7 +139,7 @@ defmodule Explorer.SmartContract.Reader do query_contract_inner(contract_address, abi, functions, block_number, nil, leave_error_as_map) end - defp query_contract_inner(contract_address, abi, functions, block_number, from, leave_error_as_map) do + defp query_contract_inner(contract_address, abi, functions, block_number, from, leave_error_as_map, options \\ []) do requests = functions |> Enum.map(fn {method_id, args} -> @@ -144,7 +153,7 @@ defmodule Explorer.SmartContract.Reader do end) requests - |> query_contracts(abi, [], leave_error_as_map) + |> query_contracts(abi, [], leave_error_as_map, options) |> Enum.zip(requests) |> Enum.into(%{}, fn {response, request} -> {request.method_id, response} @@ -170,9 +179,14 @@ defmodule Explorer.SmartContract.Reader do EthereumJSONRPC.execute_contract_functions(requests, abi, json_rpc_named_arguments) end - @spec query_contracts([Contract.call()], term(), true | false) :: [Contract.call_result()] - def query_contracts(requests, abi, [], leave_error_as_map) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + @spec query_contracts([Contract.call()], term(), contract_call_options(), true | false, Keyword.t()) :: [ + Contract.call_result() + ] + def query_contracts(requests, abi, [], leave_error_as_map, options \\ []) do + json_rpc_named_arguments = + :explorer + |> Application.get_env(:json_rpc_named_arguments) + |> Keyword.merge(options) EthereumJSONRPC.execute_contract_functions(requests, abi, json_rpc_named_arguments, leave_error_as_map) end @@ -208,28 +222,30 @@ defmodule Explorer.SmartContract.Reader do } ] """ - @spec read_only_functions(SmartContract.t(), Hash.t(), String.t() | nil) :: [%{}] - def read_only_functions(%SmartContract{abi: abi}, contract_address_hash, from) do + @spec read_only_functions(SmartContract.t(), Hash.t(), String.t() | nil, Keyword.t()) :: [%{}] + def read_only_functions(smart_contract, contract_address_hash, from, options \\ []) + + def read_only_functions(%SmartContract{abi: abi}, contract_address_hash, from, options) do case abi do nil -> [] _ -> - read_only_functions_from_abi_with_sender(abi, contract_address_hash, from) + read_only_functions_from_abi_with_sender(abi, contract_address_hash, from, options) end end - def read_only_functions(nil, _, _), do: [] + def read_only_functions(nil, _, _, _), do: [] def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string, from, options \\ []) do - implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string, options) + implementation_abi = SmartContract.get_smart_contract_abi(implementation_address_hash_string, options) case implementation_abi do nil -> [] _ -> - read_only_functions_from_abi_with_sender(implementation_abi, contract_address_hash, from) + read_only_functions_from_abi_with_sender(implementation_abi, contract_address_hash, from, options) end end @@ -238,7 +254,7 @@ defmodule Explorer.SmartContract.Reader do """ @spec read_functions_required_wallet_proxy(String.t()) :: [%{}] def read_functions_required_wallet_proxy(implementation_address_hash_string) do - implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string) + implementation_abi = SmartContract.get_smart_contract_abi(implementation_address_hash_string) case implementation_abi do nil -> @@ -265,20 +281,25 @@ defmodule Explorer.SmartContract.Reader do def read_functions_required_wallet(nil), do: [] - def read_only_functions_from_abi_with_sender([_ | _] = abi, contract_address_hash, from) do + def read_only_functions_from_abi_with_sender(abi, contract_address_hash, from, options \\ []) + + def read_only_functions_from_abi_with_sender([_ | _] = abi, contract_address_hash, from, options) do abi_with_method_id = get_abi_with_method_id(abi) abi_with_method_id |> Enum.filter(&Helper.queriable_method?(&1)) - |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false, [], from)) + |> Enum.map( + &fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false, options, from) + ) end - def read_only_functions_from_abi_with_sender(_, _, _), do: [] + def read_only_functions_from_abi_with_sender(_, _, _, _), do: [] def read_functions_required_wallet_from_abi([_ | _] = abi) do abi_with_method_id = get_abi_with_method_id(abi) abi_with_method_id + |> Enum.reject(&Helper.queriable_method?(&1)) |> Enum.filter(&Helper.read_with_wallet_method?(&1)) end @@ -389,7 +410,7 @@ defmodule Explorer.SmartContract.Reader do from, abi, leave_error_as_map, - _options + options ) do outputs = query_function_with_custom_abi_inner( @@ -398,7 +419,8 @@ defmodule Explorer.SmartContract.Reader do args || [], from, leave_error_as_map, - abi + abi, + options ) names = parse_names_from_abi(abi, method_id) @@ -439,11 +461,25 @@ defmodule Explorer.SmartContract.Reader do Hash.t(), %{method_id: String.t(), args: [term()] | nil}, String.t(), - [%{}] + [%{}], + Keyword.t() ) :: %{:names => [any()], :output => [%{}]} - def query_function_with_names_custom_abi(contract_address_hash, %{method_id: method_id, args: args}, from, custom_abi) do + def query_function_with_names_custom_abi( + contract_address_hash, + %{method_id: method_id, args: args}, + from, + custom_abi, + options \\ [] + ) do outputs = - query_function_with_custom_abi(contract_address_hash, %{method_id: method_id, args: args}, from, true, custom_abi) + query_function_with_custom_abi( + contract_address_hash, + %{method_id: method_id, args: args}, + from, + true, + custom_abi, + options + ) names = parse_names_from_abi(custom_abi, method_id) %{output: outputs, names: names} @@ -497,7 +533,8 @@ defmodule Explorer.SmartContract.Reader do %{method_id: String.t(), args: [term()] | nil | []}, String.t() | nil, true | false, - [%{}] + [%{}], + Keyword.t() ) :: [ %{} ] @@ -506,7 +543,8 @@ defmodule Explorer.SmartContract.Reader do %{method_id: method_id, args: args}, from, leave_error_as_map, - custom_abi + custom_abi, + options \\ [] ) do query_function_with_custom_abi_inner( contract_address_hash, @@ -514,11 +552,20 @@ defmodule Explorer.SmartContract.Reader do args || [], from, leave_error_as_map, - custom_abi + custom_abi, + options ) end - @spec query_function_with_custom_abi_inner(Hash.t(), String.t(), [term()], String.t() | nil, true | false, [%{}]) :: [ + @spec query_function_with_custom_abi_inner( + Hash.t(), + String.t(), + [term()], + String.t() | nil, + true | false, + [%{}], + Keyword.t() + ) :: [ %{} ] defp query_function_with_custom_abi_inner( @@ -527,7 +574,8 @@ defmodule Explorer.SmartContract.Reader do args, from, leave_error_as_map, - custom_abi + custom_abi, + options \\ [] ) do parsed_abi = custom_abi @@ -542,7 +590,8 @@ defmodule Explorer.SmartContract.Reader do custom_abi, outputs, method_id, - leave_error_as_map + leave_error_as_map, + options ) {:error, message} -> @@ -565,17 +614,26 @@ defmodule Explorer.SmartContract.Reader do end end - defp query_contract_and_link_outputs(contract_address_hash, args, from, abi, outputs, method_id, leave_error_as_map) do + defp query_contract_and_link_outputs( + contract_address_hash, + args, + from, + abi, + outputs, + method_id, + leave_error_as_map, + options \\ [] + ) do contract_address_hash - |> query_verified_contract(%{method_id => normalize_args(args)}, from, leave_error_as_map, abi) + |> query_verified_contract(%{method_id => normalize_args(args)}, from, leave_error_as_map, abi, options) |> link_outputs_and_values(outputs, method_id) end defp get_abi(contract_address_hash, type, options) do - contract = Chain.address_hash_to_smart_contract(contract_address_hash, options) + contract = SmartContract.address_hash_to_smart_contract(contract_address_hash, options) if type == :proxy do - Chain.get_implementation_abi_from_proxy(contract, options) + Proxy.get_implementation_abi_from_proxy(contract, options) else contract.abi end diff --git a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex index 799fd7bd0e0a..2c5629d219c0 100644 --- a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex +++ b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex @@ -5,7 +5,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do defmacro __using__(_) do # credo:disable-for-next-line quote([]) do - alias Explorer.Utility.RustService + alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger @@ -22,9 +22,9 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do "optimizationRuns" => _, "libraries" => _ } = body, - address_hash + metadata ) do - http_post_request(solidity_multiple_files_verification_url(), append_metadata(body, address_hash)) + http_post_request(solidity_multiple_files_verification_url(), append_metadata(body, metadata), true) end def verify_standard_json_input( @@ -34,9 +34,9 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do "compilerVersion" => _, "input" => _ } = body, - address_hash + metadata ) do - http_post_request(solidity_standard_json_verification_url(), append_metadata(body, address_hash)) + http_post_request(solidity_standard_json_verification_url(), append_metadata(body, metadata), true) end def vyper_verify_multipart( @@ -46,9 +46,9 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do "compilerVersion" => _, "sourceFiles" => _ } = body, - address_hash + metadata ) do - http_post_request(vyper_multiple_files_verification_url(), append_metadata(body, address_hash)) + http_post_request(vyper_multiple_files_verification_url(), append_metadata(body, metadata), true) end def vyper_verify_standard_json( @@ -58,15 +58,17 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do "compilerVersion" => _, "input" => _ } = body, - address_hash + metadata ) do - http_post_request(vyper_standard_json_verification_url(), append_metadata(body, address_hash)) + http_post_request(vyper_standard_json_verification_url(), append_metadata(body, metadata), true) end - def http_post_request(url, body) do + def http_post_request(url, body, is_verification_request? \\ false) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + case HTTPoison.post(url, Jason.encode!(body), maybe_put_api_key_header(headers, is_verification_request?), + recv_timeout: @post_timeout + ) do {:ok, %Response{body: body, status_code: _}} -> process_verifier_response(body) @@ -86,6 +88,18 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do end end + defp maybe_put_api_key_header(headers, false), do: headers + + defp maybe_put_api_key_header(headers, true) do + api_key = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:api_key] + + if api_key do + [{"x-api-key", api_key} | headers] + else + headers + end + end + def http_get_request(url) do case HTTPoison.get(url) do {:ok, %Response{body: body, status_code: 200}} -> @@ -158,17 +172,14 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do def base_api_url, do: "#{base_url()}" <> "/api/v2" def base_url do - RustService.base_url(Explorer.SmartContract.RustVerifierInterfaceBehaviour) + Microservice.base_url(Explorer.SmartContract.RustVerifierInterfaceBehaviour) end def enabled?, do: Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:enabled] - defp append_metadata(body, address_hash) when is_map(body) do + defp append_metadata(body, metadata) when is_map(body) do body - |> Map.put("metadata", %{ - "chainId" => Application.get_env(:block_scout_web, :chain_id), - "contractAddress" => to_string(address_hash) - }) + |> Map.put("metadata", metadata) end end end diff --git a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex index 92bba0d0ec70..b97f9bcd1edf 100644 --- a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex @@ -3,7 +3,7 @@ defmodule Explorer.SmartContract.SigProviderInterface do Adapter for decoding events and function calls with https://github.com/blockscout/blockscout-rs/tree/main/sig-provider """ - alias Explorer.Utility.RustService + alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger @@ -81,7 +81,7 @@ defmodule Explorer.SmartContract.SigProviderInterface do def base_api_url, do: "#{base_url()}" <> "/api/v1/abi" def base_url do - RustService.base_url(__MODULE__) + Microservice.base_url(__MODULE__) end def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex index cee27ad6c942..a81a0f7ab5d0 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex @@ -5,9 +5,8 @@ defmodule Explorer.SmartContract.Solidity.Publisher do require Logger - import Explorer.SmartContract.Helper, only: [cast_libraries: 1] + import Explorer.SmartContract.Helper, only: [cast_libraries: 1, prepare_license_type: 1] - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.{CompilerVersion, Helper} alias Explorer.SmartContract.Solidity.Verifier @@ -49,7 +48,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do "sourceFiles" => _ } = result_params } -> - process_rust_verifier_response(result_params, address_hash, false, false) + process_rust_verifier_response(result_params, address_hash, params, false, false) {:ok, %{abi: abi, constructor_arguments: constructor_arguments}} -> params_with_constructor_arguments = @@ -85,7 +84,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do "sourceFiles" => _, "compilerSettings" => _ } = result_params} -> - process_rust_verifier_response(result_params, address_hash, true, true) + process_rust_verifier_response(result_params, address_hash, params, true, true) {:ok, %{abi: abi, constructor_arguments: constructor_arguments}, additional_params} -> params_with_constructor_arguments = @@ -125,7 +124,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do "sourceFiles" => _, "compilerSettings" => _ } = result_params} -> - process_rust_verifier_response(result_params, address_hash, false, true) + process_rust_verifier_response(result_params, address_hash, params, false, true) {:error, error} -> {:error, unverified_smart_contract(address_hash, params, error, nil, true)} @@ -147,6 +146,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do "matchType" => match_type } = source, address_hash, + initial_params, is_standard_json?, save_file_path?, automatically_verified? \\ false @@ -178,6 +178,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do |> Map.put("partially_verified", match_type == "PARTIAL") |> Map.put("verified_via_eth_bytecode_db", automatically_verified?) |> Map.put("verified_via_sourcify", source["sourcify?"]) + |> Map.put("license_type", initial_params["license_type"]) publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string || "null")) end @@ -200,10 +201,10 @@ defmodule Explorer.SmartContract.Solidity.Publisher do defp create_or_update_smart_contract(address_hash, attrs) do Logger.info("Publish successfully verified Solidity smart-contract #{address_hash} into the DB") - if Chain.smart_contract_verified?(address_hash) do - Chain.update_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) + if SmartContract.verified?(address_hash) do + SmartContract.update_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) else - Chain.create_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) + SmartContract.create_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) end end @@ -272,7 +273,8 @@ defmodule Explorer.SmartContract.Solidity.Publisher do autodetect_constructor_args: params["autodetect_constructor_args"], is_yul: params["is_yul"] || false, compiler_settings: clean_compiler_settings, - verified_via_eth_bytecode_db: params["verified_via_eth_bytecode_db"] || false + verified_via_eth_bytecode_db: params["verified_via_eth_bytecode_db"] || false, + license_type: prepare_license_type(params["license_type"]) || :none } end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex index b9ae134d54cb..ae2caacc5e5a 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher_worker.ex @@ -49,15 +49,12 @@ defmodule Explorer.SmartContract.Solidity.PublisherWorker do end def perform({"json_api", %{"address_hash" => address_hash} = params, json_input, uid}) when is_binary(uid) do - VerificationStatus.insert_status(uid, :pending, address_hash) - - case Publisher.publish_with_standard_json_input(params, json_input) do - {:ok, _contract} -> - VerificationStatus.update_status(uid, :pass) + publish_and_update_status(:publish_with_standard_json_input, [params, json_input], address_hash, uid) + end - {:error, _changeset} -> - VerificationStatus.update_status(uid, :fail) - end + def perform({"flattened_api", %{"address_hash" => address_hash} = params, external_libraries, uid}) + when is_binary(uid) do + publish_and_update_status(:publish, [address_hash, params, external_libraries], address_hash, uid) end defp broadcast(method, address_hash, args, conn \\ nil) do @@ -67,6 +64,10 @@ defmodule Explorer.SmartContract.Solidity.PublisherWorker do result {:error, changeset} -> + Logger.error( + "Solidity smart-contract verification #{address_hash} failed because of the error: #{inspect(changeset)}" + ) + {:error, changeset} end @@ -78,4 +79,16 @@ defmodule Explorer.SmartContract.Solidity.PublisherWorker do EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result}}], :on_demand) end end + + defp publish_and_update_status(method, args, address_hash, uid) do + VerificationStatus.insert_status(uid, :pending, address_hash) + + case apply(Publisher, method, args) do + {:ok, _contract} -> + VerificationStatus.update_status(uid, :pass) + + {:error, _changeset} -> + VerificationStatus.update_status(uid, :fail) + end + end end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex index ee5554a0361d..8eb602ba3559 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex @@ -9,9 +9,12 @@ defmodule Explorer.SmartContract.Solidity.Verifier do """ import Explorer.SmartContract.Helper, - only: [cast_libraries: 1, prepare_bytecode_for_microservice: 3, contract_creation_input: 1] + only: [ + cast_libraries: 1, + fetch_data_for_verification: 1, + prepare_bytecode_for_microservice: 3 + ] - # import Explorer.Chain.SmartContract, only: [:function_description] alias ABI.{FunctionSelector, TypeDecoder} alias Explorer.Chain alias Explorer.Chain.{Data, Hash, SmartContract} @@ -40,9 +43,7 @@ defmodule Explorer.SmartContract.Solidity.Verifier do end defp evaluate_authenticity_inner(true, address_hash, params) do - deployed_bytecode = Chain.smart_contract_bytecode(address_hash) - - creation_tx_input = contract_creation_input(address_hash) + {creation_tx_input, deployed_bytecode, verifier_metadata} = fetch_data_for_verification(address_hash) %{} |> prepare_bytecode_for_microservice(creation_tx_input, deployed_bytecode) @@ -54,7 +55,7 @@ defmodule Explorer.SmartContract.Solidity.Verifier do |> Map.put("optimizationRuns", prepare_optimization_runs(params["optimization"], params["optimization_runs"])) |> Map.put("evmVersion", Map.get(params, "evm_version", "default")) |> Map.put("compilerVersion", params["compiler_version"]) - |> RustVerifierInterface.verify_multi_part(address_hash) + |> RustVerifierInterface.verify_multi_part(verifier_metadata) end defp evaluate_authenticity_inner(false, address_hash, params) do @@ -124,14 +125,12 @@ defmodule Explorer.SmartContract.Solidity.Verifier do end def evaluate_authenticity_via_standard_json_input_inner(true, address_hash, params, json_input) do - deployed_bytecode = Chain.smart_contract_bytecode(address_hash) - - creation_tx_input = contract_creation_input(address_hash) + {creation_tx_input, deployed_bytecode, verifier_metadata} = fetch_data_for_verification(address_hash) %{"compilerVersion" => params["compiler_version"]} |> prepare_bytecode_for_microservice(creation_tx_input, deployed_bytecode) |> Map.put("input", json_input) - |> RustVerifierInterface.verify_standard_json_input(address_hash) + |> RustVerifierInterface.verify_standard_json_input(verifier_metadata) end def evaluate_authenticity_via_standard_json_input_inner(false, address_hash, params, json_input) do @@ -139,9 +138,7 @@ defmodule Explorer.SmartContract.Solidity.Verifier do end def evaluate_authenticity_via_multi_part_files(address_hash, params, files) do - deployed_bytecode = Chain.smart_contract_bytecode(address_hash) - - creation_tx_input = contract_creation_input(address_hash) + {creation_tx_input, deployed_bytecode, verifier_metadata} = fetch_data_for_verification(address_hash) %{} |> prepare_bytecode_for_microservice(creation_tx_input, deployed_bytecode) @@ -150,7 +147,7 @@ defmodule Explorer.SmartContract.Solidity.Verifier do |> Map.put("optimizationRuns", prepare_optimization_runs(params["optimization"], params["optimization_runs"])) |> Map.put("evmVersion", Map.get(params, "evm_version", "default")) |> Map.put("compilerVersion", params["compiler_version"]) - |> RustVerifierInterface.verify_multi_part(address_hash) + |> RustVerifierInterface.verify_multi_part(verifier_metadata) end defp verify(address_hash, params, json_input) do diff --git a/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex b/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex index e5c38116d06f..3a27818dc4ae 100644 --- a/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex +++ b/apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex @@ -3,11 +3,10 @@ defmodule Explorer.SmartContract.Vyper.Publisher do Module responsible to control Vyper contract verification. """ - import Explorer.SmartContract.Helper, only: [cast_libraries: 1] + import Explorer.SmartContract.Helper, only: [cast_libraries: 1, prepare_license_type: 1] require Logger - alias Explorer.Chain alias Explorer.Chain.SmartContract alias Explorer.SmartContract.CompilerVersion alias Explorer.SmartContract.Vyper.Verifier @@ -26,7 +25,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do "compilerSettings" => _compiler_settings_string } = source } -> - process_rust_verifier_response(source, address_hash, false, false) + process_rust_verifier_response(source, address_hash, params, false, false) {:ok, %{abi: abi}} -> publish_smart_contract(address_hash, params, abi) @@ -61,7 +60,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do "compilerSettings" => _compiler_settings_string } = source } -> - process_rust_verifier_response(source, address_hash, true, standard_json?) + process_rust_verifier_response(source, address_hash, params, true, standard_json?) {:ok, %{abi: abi}} -> publish_smart_contract(address_hash, params, abi) @@ -86,6 +85,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do "matchType" => match_type }, address_hash, + initial_params, save_file_path?, standard_json?, automatically_verified? \\ false @@ -116,6 +116,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do if(is_nil(compiler_settings["optimize"]), do: true, else: compiler_settings["optimize"]) ) |> Map.put("compiler_settings", if(standard_json?, do: compiler_settings)) + |> Map.put("license_type", initial_params["license_type"]) publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string)) end @@ -124,7 +125,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do Logger.info("Publish successfully verified Vyper smart-contract #{address_hash} into the DB") attrs = address_hash |> attributes(params, abi) - Chain.create_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) + SmartContract.create_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) end defp unverified_smart_contract(address_hash, params, error, error_message, verification_with_files? \\ false) do @@ -183,7 +184,8 @@ defmodule Explorer.SmartContract.Vyper.Publisher do is_vyper_contract: true, file_path: params["file_path"], verified_via_eth_bytecode_db: params["verified_via_eth_bytecode_db"] || false, - compiler_settings: clean_compiler_settings + compiler_settings: clean_compiler_settings, + license_type: prepare_license_type(params["license_type"]) || :none } end end diff --git a/apps/explorer/lib/explorer/smart_contract/vyper/verifier.ex b/apps/explorer/lib/explorer/smart_contract/vyper/verifier.ex index 3d66e1539a01..3ca5ac90994c 100644 --- a/apps/explorer/lib/explorer/smart_contract/vyper/verifier.ex +++ b/apps/explorer/lib/explorer/smart_contract/vyper/verifier.ex @@ -9,10 +9,11 @@ defmodule Explorer.SmartContract.Vyper.Verifier do """ require Logger - alias Explorer.Chain alias Explorer.SmartContract.Vyper.CodeCompiler alias Explorer.SmartContract.RustVerifierInterface - import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, contract_creation_input: 1] + + import Explorer.SmartContract.Helper, + only: [fetch_data_for_verification: 1, prepare_bytecode_for_microservice: 3, contract_creation_input: 1] def evaluate_authenticity(_, %{"contract_source_code" => ""}), do: {:error, :contract_source_code} @@ -34,7 +35,7 @@ defmodule Explorer.SmartContract.Vyper.Verifier do def evaluate_authenticity(address_hash, params, files) do try do if RustVerifierInterface.enabled?() do - vyper_verify_multipart(params, fetch_bytecode(address_hash), params["evm_version"], files, address_hash) + vyper_verify_multipart(params, params["evm_version"], files, address_hash) end rescue exception -> @@ -50,7 +51,7 @@ defmodule Explorer.SmartContract.Vyper.Verifier do def evaluate_authenticity_standard_json(%{"address_hash" => address_hash} = params) do try do if RustVerifierInterface.enabled?() do - vyper_verify_standard_json(params, fetch_bytecode(address_hash), address_hash) + vyper_verify_standard_json(params, address_hash) end rescue exception -> @@ -66,7 +67,6 @@ defmodule Explorer.SmartContract.Vyper.Verifier do defp evaluate_authenticity_inner(true, address_hash, params) do vyper_verify_multipart( params, - fetch_bytecode(address_hash), params["evm_version"], %{ "#{params["name"]}.vy" => params["contract_source_code"] @@ -79,13 +79,6 @@ defmodule Explorer.SmartContract.Vyper.Verifier do verify(address_hash, params) end - def fetch_bytecode(address_hash) do - deployed_bytecode = Chain.smart_contract_bytecode(address_hash) - creation_tx_input = contract_creation_input(address_hash) - - prepare_bytecode_for_microservice(%{}, creation_tx_input, deployed_bytecode) - end - defp verify(address_hash, params) do contract_source_code = Map.fetch!(params, "contract_source_code") compiler_version = Map.fetch!(params, "compiler_version") @@ -123,19 +116,25 @@ defmodule Explorer.SmartContract.Vyper.Verifier do end end - defp vyper_verify_multipart(params, bytecode_map, evm_version, files, address_hash) do - bytecode_map + defp vyper_verify_multipart(params, evm_version, files, address_hash) do + {creation_tx_input, deployed_bytecode, verifier_metadata} = fetch_data_for_verification(address_hash) + + %{} + |> prepare_bytecode_for_microservice(creation_tx_input, deployed_bytecode) |> Map.put("evmVersion", evm_version) |> Map.put("sourceFiles", files) |> Map.put("compilerVersion", params["compiler_version"]) |> Map.put("interfaces", params["interfaces"] || %{}) - |> RustVerifierInterface.vyper_verify_multipart(address_hash) + |> RustVerifierInterface.vyper_verify_multipart(verifier_metadata) end - defp vyper_verify_standard_json(params, bytecode_map, address_hash) do - bytecode_map + defp vyper_verify_standard_json(params, address_hash) do + {creation_tx_input, deployed_bytecode, verifier_metadata} = fetch_data_for_verification(address_hash) + + %{} + |> prepare_bytecode_for_microservice(creation_tx_input, deployed_bytecode) |> Map.put("compilerVersion", params["compiler_version"]) |> Map.put("input", params["input"]) - |> RustVerifierInterface.vyper_verify_standard_json(address_hash) + |> RustVerifierInterface.vyper_verify_standard_json(verifier_metadata) end end diff --git a/apps/explorer/lib/explorer/smart_contract/writer.ex b/apps/explorer/lib/explorer/smart_contract/writer.ex index 07b77abd627b..6aef776a1ca5 100644 --- a/apps/explorer/lib/explorer/smart_contract/writer.ex +++ b/apps/explorer/lib/explorer/smart_contract/writer.ex @@ -3,7 +3,6 @@ defmodule Explorer.SmartContract.Writer do Generates smart-contract transactions """ - alias Explorer.Chain alias Explorer.Chain.{Hash, SmartContract} alias Explorer.SmartContract.Helper @@ -21,7 +20,7 @@ defmodule Explorer.SmartContract.Writer do @spec write_functions_proxy(Hash.t() | String.t()) :: [%{}] def write_functions_proxy(implementation_address_hash_string, options \\ []) do - implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string, options) + implementation_abi = SmartContract.get_smart_contract_abi(implementation_address_hash_string, options) case implementation_abi do nil -> diff --git a/apps/explorer/lib/explorer/sorting_helper.ex b/apps/explorer/lib/explorer/sorting_helper.ex new file mode 100644 index 000000000000..1c0e064b2e13 --- /dev/null +++ b/apps/explorer/lib/explorer/sorting_helper.ex @@ -0,0 +1,166 @@ +defmodule Explorer.SortingHelper do + @moduledoc """ + Module that order and paginate queries dynamically based on default and provided sorting parameters. + Example of sorting parameters: + ``` + [{:asc, :fetched_coin_balance, :address}, {:dynamic, :contract_code_size, :desc, dynamic([t], fragment(LENGTH(?), t.contract_source_code))}, desc: :id] + ``` + First list entry specify joined address table column as a column to order by and paginate, second entry + specifies name of a key in paging_options and arbitrary dynamic that will be used in ordering and pagination, + third entry specifies own column name to order by and paginate. + """ + alias Explorer.PagingOptions + + import Ecto.Query + + @typep ordering :: :asc | :asc_nulls_first | :asc_nulls_last | :desc | :desc_nulls_first | :desc_nulls_last + @typep column :: atom + @typep binding :: atom + @type sorting_params :: [ + {ordering, column} | {ordering, column, binding} | {:dynamic, column, ordering, Ecto.Query.dynamic_expr()} + ] + + @doc """ + Applies sorting to query based on default sorting params and sorting params from the client, + these params merged keeping provided one over default one. + """ + @spec apply_sorting(Ecto.Query.t(), sorting_params, sorting_params) :: Ecto.Query.t() + def apply_sorting(query, sorting, default_sorting) when is_list(sorting) and is_list(default_sorting) do + sorting |> merge_sorting_params_with_defaults(default_sorting) |> sorting_params_to_order_by(query) + end + + defp merge_sorting_params_with_defaults([], default_sorting) when is_list(default_sorting), do: default_sorting + + defp merge_sorting_params_with_defaults(sorting, default_sorting) + when is_list(sorting) and is_list(default_sorting) do + (sorting ++ default_sorting) + |> Enum.uniq_by(fn + {_, field} -> field + {_, field, as} -> {field, as} + {:dynamic, key_name, _, _} -> key_name + end) + end + + defp sorting_params_to_order_by(sorting_params, query) do + sorting_params + |> Enum.reduce(query, fn + {:dynamic, _key_name, order, dynamic}, query -> query |> order_by(^[{order, dynamic}]) + {order, column, binding}, query -> query |> order_by([{^order, field(as(^binding), ^column)}]) + {order, column}, query -> query |> order_by(^[{order, column}]) + end) + end + + @doc """ + Page the query based on paging options, default sorting params and sorting params from the client, + these params merged keeping provided one over default one. + """ + @spec page_with_sorting(Ecto.Query.t(), PagingOptions.t(), sorting_params, sorting_params) :: Ecto.Query.t() + def page_with_sorting(query, %PagingOptions{key: key, page_size: page_size}, sorting, default_sorting) + when not is_nil(key) do + sorting + |> merge_sorting_params_with_defaults(default_sorting) + |> do_page_with_sorting() + |> case do + nil -> query + dynamic_where -> query |> where(^dynamic_where.(key)) + end + |> limit_query(page_size) + end + + def page_with_sorting(query, %PagingOptions{page_size: page_size}, _sorting, _default_sorting) do + query |> limit_query(page_size) + end + + def page_with_sorting(query, _, _sorting, _default_sorting), do: query + + defp limit_query(query, limit) when is_integer(limit), do: query |> limit(^limit) + defp limit_query(query, _), do: query + + defp do_page_with_sorting([{order, column} | rest]) do + fn key -> page_by_column(key, column, order, do_page_with_sorting(rest)) end + end + + defp do_page_with_sorting([{:dynamic, key_name, order, dynamic} | rest]) do + fn key -> page_by_column(key, {:dynamic, key_name, dynamic}, order, do_page_with_sorting(rest)) end + end + + defp do_page_with_sorting([{order, column, binding} | rest]) do + fn key -> page_by_column(key, {column, binding}, order, do_page_with_sorting(rest)) end + end + + defp do_page_with_sorting([]), do: nil + + for {key_name, pattern, ecto_value} <- [ + {quote(do: key_name), quote(do: {:dynamic, key_name, dynamic}), quote(do: ^dynamic)}, + {quote(do: column), quote(do: {column, binding}), quote(do: field(as(^binding), ^column))}, + {quote(do: column), quote(do: column), quote(do: field(t, ^column))} + ] do + defp page_by_column(key, unquote(pattern), :desc_nulls_last, next_column) do + case key[unquote(key_name)] do + nil -> + dynamic([t], is_nil(unquote(ecto_value)) and ^apply_next_column(next_column, key)) + + value -> + dynamic( + [t], + is_nil(unquote(ecto_value)) or unquote(ecto_value) < ^value or + (unquote(ecto_value) == ^value and ^apply_next_column(next_column, key)) + ) + end + end + + defp page_by_column(key, unquote(pattern), :asc_nulls_first, next_column) do + case key[unquote(key_name)] do + nil -> + dynamic([t], not is_nil(unquote(ecto_value)) or ^apply_next_column(next_column, key)) + + value -> + dynamic( + [t], + not is_nil(unquote(ecto_value)) and + (unquote(ecto_value) > ^value or + (unquote(ecto_value) == ^value and ^apply_next_column(next_column, key))) + ) + end + end + + defp page_by_column(key, unquote(pattern), order, next_column) when order in ~w(asc asc_nulls_last)a do + case key[unquote(key_name)] do + nil -> + dynamic([t], is_nil(unquote(ecto_value)) and ^apply_next_column(next_column, key)) + + value -> + dynamic( + [t], + is_nil(unquote(ecto_value)) or + (unquote(ecto_value) > ^value or + (unquote(ecto_value) == ^value and ^apply_next_column(next_column, key))) + ) + end + end + + defp page_by_column(key, unquote(pattern), order, next_column) + when order in ~w(desc desc_nulls_first)a do + case key[unquote(key_name)] do + nil -> + dynamic([t], not is_nil(unquote(ecto_value)) or ^apply_next_column(next_column, key)) + + value -> + dynamic( + [t], + not is_nil(unquote(ecto_value)) and + (unquote(ecto_value) < ^value or + (unquote(ecto_value) == ^value and ^apply_next_column(next_column, key))) + ) + end + end + end + + defp apply_next_column(nil, _key) do + false + end + + defp apply_next_column(next_column, key) do + next_column.(key) + end +end diff --git a/apps/explorer/lib/explorer/tags/address_tag.ex b/apps/explorer/lib/explorer/tags/address_tag.ex index cfb61d3324b7..65d7cc008baf 100644 --- a/apps/explorer/lib/explorer/tags/address_tag.ex +++ b/apps/explorer/lib/explorer/tags/address_tag.ex @@ -20,13 +20,9 @@ defmodule Explorer.Tags.AddressTag do * `:label` - Tag's label * `:display_name` - Label's display name """ - @type t :: %AddressTag{ - label: String.t() - } - - schema "address_tags" do - field(:label, :string) - field(:display_name, :string) + typed_schema "address_tags" do + field(:label, :string, null: false) + field(:display_name, :string, null: false) has_many(:tag_id, AddressToTag, foreign_key: :id) timestamps() diff --git a/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex index a8586313a612..da54d81e2d88 100644 --- a/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex +++ b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex @@ -165,7 +165,11 @@ defmodule Explorer.Tags.AddressTag.Cataloger do defp set_omni_tag do set_tag_for_multiple_env_var_addresses( - ["ETH_OMNI_BRIDGE_MEDIATOR", "BSC_OMNI_BRIDGE_MEDIATOR", "POA_OMNI_BRIDGE_MEDIATOR"], + [ + "BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR", + "BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR", + "BRIDGED_TOKENS_POA_OMNI_BRIDGE_MEDIATOR" + ], "omni bridge" ) end diff --git a/apps/explorer/lib/explorer/tags/address_to_tag.ex b/apps/explorer/lib/explorer/tags/address_to_tag.ex index 26468cf29abb..9c2f8d410ca1 100644 --- a/apps/explorer/lib/explorer/tags/address_to_tag.ex +++ b/apps/explorer/lib/explorer/tags/address_to_tag.ex @@ -17,18 +17,14 @@ defmodule Explorer.Tags.AddressToTag do * `:tag_id` - id of Tag * `:address_hash` - hash of Address """ - @type t :: %AddressToTag{ - tag_id: Decimal.t(), - address_hash: Hash.Address.t() - } - - schema "address_to_tags" do + typed_schema "address_to_tags" do belongs_to( :tag, AddressTag, foreign_key: :tag_id, references: :id, - type: :integer + type: :integer, + null: false ) belongs_to( @@ -36,7 +32,8 @@ defmodule Explorer.Tags.AddressToTag do Address, foreign_key: :address_hash, references: :hash, - type: Hash.Address + type: Hash.Address, + null: false ) timestamps() @@ -94,7 +91,7 @@ defmodule Explorer.Tags.AddressToTag do addresses_to_add |> Enum.map(fn address_hash_string -> with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - :ok <- Chain.check_address_exists(address_hash) do + :ok <- Address.check_address_exists(address_hash) do %{ tag_id: tag_id, address_hash: address_hash, diff --git a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex index a2aba9c6640c..f7e178002e86 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex @@ -1,14 +1,20 @@ defmodule Explorer.ThirdPartyIntegrations.AirTable do @moduledoc """ - Module is responsible for submitting requests for public tags to AirTable + Module is responsible for submitting requests for public tags and audit reports to AirTable """ require Logger alias Ecto.Changeset alias Explorer.Account.PublicTagsRequest + alias Explorer.Chain.SmartContract.AuditReport alias Explorer.Repo alias HTTPoison.Response + @doc """ + Submits a public tags request or audit report to AirTable + """ + @spec submit({:ok, PublicTagsRequest.t()} | {:error, Changeset.t()} | Changeset.t()) :: + {:ok, PublicTagsRequest.t()} | {:error, Changeset.t()} | Changeset.t() def submit({:ok, %PublicTagsRequest{} = new_request} = input) do if Mix.env() == :test do new_request @@ -17,30 +23,17 @@ defmodule Explorer.ThirdPartyIntegrations.AirTable do input else - api_key = Application.get_env(:explorer, __MODULE__)[:api_key] - headers = [{"Authorization", "Bearer #{api_key}"}, {"Content-Type", "application/json"}] - url = Application.get_env(:explorer, __MODULE__)[:table_url] - - body = %{ - "typecast" => true, - "records" => [%{"fields" => PublicTagsRequest.to_map(new_request)}] - } - - request = HTTPoison.post(url, Jason.encode!(body), headers, []) - - case request do - {:ok, %Response{body: body, status_code: 200}} -> - request_id = Enum.at(Jason.decode!(body)["records"], 0)["fields"]["request_id"] - + submit_entry( + PublicTagsRequest.to_map(new_request), + :air_table_public_tags, + fn request_id -> new_request |> PublicTagsRequest.changeset(%{request_id: request_id}) |> Repo.account_repo().update() input - - error -> - Logger.error(fn -> ["Error while submitting AirTable entry", inspect(error)] end) - + end, + fn -> {:error, %{ (%PublicTagsRequest{} @@ -48,9 +41,53 @@ defmodule Explorer.ThirdPartyIntegrations.AirTable do |> Changeset.add_error(:full_name, "AirTable error. Please try again later")) | action: :insert }} - end + end + ) end end - def submit(error), do: error + def submit(%Changeset{} = changeset), do: submit(Changeset.apply_action(changeset, :insert), changeset) + + def submit({:ok, %AuditReport{} = audit_report}, changeset) do + submit_entry( + AuditReport.to_map(audit_report), + :air_table_audit_reports, + fn request_id -> + changeset + |> Changeset.put_change(:request_id, request_id) + end, + fn -> + changeset + |> Changeset.add_error(:smart_contract_address_hash, "AirTable error. Please try again later") + end + ) + end + + def submit(_error, changeset), do: changeset + + defp submit_entry(map, envs_key, success_callback, failure_callback) do + envs = Application.get_env(:explorer, envs_key) + api_key = envs[:api_key] + headers = [{"Authorization", "Bearer #{api_key}"}, {"Content-Type", "application/json"}] + url = envs[:table_url] + + body = %{ + "typecast" => true, + "records" => [%{"fields" => map}] + } + + request = HTTPoison.post(url, Jason.encode!(body), headers, []) + + case request do + {:ok, %Response{body: body, status_code: 200}} -> + request_id = Enum.at(Jason.decode!(body)["records"], 0)["fields"]["request_id"] + + success_callback.(request_id) + + error -> + Logger.error(fn -> ["Error while submitting AirTable entry", inspect(error)] end) + + failure_callback.() + end + end end diff --git a/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex b/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex new file mode 100644 index 000000000000..c63501cc071d --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex @@ -0,0 +1,90 @@ +defmodule Explorer.ThirdPartyIntegrations.NovesFi do + @moduledoc """ + Module for Noves.Fi API integration https://blockscout.noves.fi/swagger/index.html + """ + + alias Explorer.Helper + alias Explorer.Utility.Microservice + + @recv_timeout 60_000 + + @doc """ + Proxy request to noves.fi API endpoints + """ + @spec noves_fi_api_request(String.t(), Plug.Conn.t(), :get | :post_transactions) :: {any(), integer()} + def noves_fi_api_request(url, conn, method \\ :get) + + def noves_fi_api_request(url, conn, :post_transactions) do + headers = [{"apiKey", api_key()}, {"Content-Type", "application/json"}, {"accept", "text/plain"}] + + hashes = + conn.query_params + |> Map.get("hashes") + |> (&if(is_map(&1), + do: Map.values(&1), + else: String.split(&1, ",") + )).() + + prepared_params = + conn.query_params + |> Map.drop(["hashes"]) + + case HTTPoison.post(url, Jason.encode!(hashes), headers, recv_timeout: @recv_timeout, params: prepared_params) do + {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + {Helper.decode_json(body), status} + + _ -> + {nil, 500} + end + end + + def noves_fi_api_request(url, conn, :get) do + headers = [{"apiKey", api_key()}] + + url_with_params = url <> "?" <> conn.query_string + + case HTTPoison.get(url_with_params, headers, recv_timeout: @recv_timeout) do + {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + {Helper.decode_json(body), status} + + _ -> + {nil, 500} + end + end + + @doc """ + Noves.fi /evm/{chain}/tx/{txHash} endpoint + """ + @spec tx_url(String.t()) :: String.t() + def tx_url(transaction_hash_string) do + "#{base_url()}/evm/#{chain_name()}/tx/#{transaction_hash_string}" + end + + @doc """ + Noves.fi /evm/{chain}/describeTxs endpoint + """ + @spec describe_txs_url() :: String.t() + def describe_txs_url do + "#{base_url()}/evm/#{chain_name()}/describeTxs" + end + + @doc """ + Noves.fi /evm/{chain}/txs/{accountAddress} endpoint + """ + @spec address_txs_url(String.t()) :: String.t() + def address_txs_url(address_hash_string) do + "#{base_url()}/evm/#{chain_name()}/txs/#{address_hash_string}" + end + + defp base_url do + Microservice.base_url(__MODULE__) + end + + defp chain_name do + Application.get_env(:explorer, __MODULE__)[:chain_name] + end + + defp api_key do + Application.get_env(:explorer, __MODULE__)[:api_key] + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex b/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex new file mode 100644 index 000000000000..27a53f5b6a2e --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex @@ -0,0 +1,53 @@ +defmodule Explorer.ThirdPartyIntegrations.SolidityScan do + @moduledoc """ + Module for SolidityScan integration https://apidoc.solidityscan.com/solidityscan-security-api/solidityscan-other-apis/quickscan-api-v1 + """ + + require Logger + alias Explorer.Helper + + @blockscout_platform_id "16" + @recv_timeout 60_000 + + @doc """ + Proxy request to solidityscan API endpoint for the given smart-contract + """ + @spec solidityscan_request(String.t()) :: any() + def solidityscan_request(address_hash_string) do + headers = [{"Authorization", "Token #{api_key()}"}] + + url = base_url(address_hash_string) + + if url do + case HTTPoison.get(url, headers, recv_timeout: @recv_timeout) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + Helper.decode_json(body) + + _ -> + nil + end + else + Logger.warning( + "SOLIDITYSCAN_CHAIN_ID or SOLIDITYSCAN_API_TOKEN env variable is not configured on the backend. Please, set it." + ) + + nil + end + end + + defp base_url(address_hash_string) do + if chain_id() && api_key() do + "https://api.solidityscan.com/api/v1/quickscan/#{@blockscout_platform_id}/#{chain_id()}/#{address_hash_string}" + else + nil + end + end + + defp chain_id do + Application.get_env(:explorer, __MODULE__)[:chain_id] + end + + defp api_key do + Application.get_env(:explorer, __MODULE__)[:api_key] + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex index b510fb355d49..605a50547ebd 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex @@ -4,6 +4,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do """ use Tesla + alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.{Helper, RustVerifierInterface} alias HTTPoison.{Error, Response} alias Tesla.Multipart @@ -223,7 +224,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_verify_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do # Success status from native Sourcify server @@ -246,7 +247,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_check_by_address_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do [%{"status" => "perfect"}] -> @@ -264,11 +265,11 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_get_metadata_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do %{"message" => message, "errors" => errors} -> - {:error, "#{message}: #{decode_json(errors)}"} + {:error, "#{message}: #{ExplorerHelper.decode_json(errors)}"} metadata -> {:ok, metadata} @@ -276,11 +277,11 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_get_metadata_any_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do %{"message" => message, "errors" => errors} -> - {:error, "#{message}: #{decode_json(errors)}"} + {:error, "#{message}: #{ExplorerHelper.decode_json(errors)}"} %{"status" => status, "files" => metadata} -> {:ok, status, metadata} @@ -290,16 +291,23 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end end + @invalid_json_response "invalid http error json response" defp parse_http_error_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) if is_map(body_json) do - {:error, body_json["error"]} + error = body_json["error"] + + parse_http_error_response_internal(error) else - {:error, body} + parse_http_error_response_internal(body) end end + defp parse_http_error_response_internal(nil), do: {:error, @invalid_json_response} + + defp parse_http_error_response_internal(data), do: {:error, data} + def parse_params_from_sourcify(address_hash_string, verification_metadata) do filtered_files = verification_metadata @@ -350,7 +358,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do defp parse_json_from_sourcify_for_insertion(verification_metadata_json) do %{"name" => _, "content" => content} = verification_metadata_json - content_json = decode_json(content) + content_json = ExplorerHelper.decode_json(content) compiler_version = "v" <> (content_json |> Map.get("compiler") |> Map.get("version")) abi = content_json |> Map.get("output") |> Map.get("abi") settings = Map.get(content_json, "settings") @@ -401,12 +409,6 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do |> Map.put("contract_source_code", content) end - def decode_json(data) do - Jason.decode!(data) - rescue - _ -> data - end - defp config(module, key) do :explorer |> Application.get_env(module) diff --git a/apps/explorer/lib/explorer/token/balance_reader.ex b/apps/explorer/lib/explorer/token/balance_reader.ex index c8577c457839..2617cfb02992 100644 --- a/apps/explorer/lib/explorer/token/balance_reader.ex +++ b/apps/explorer/lib/explorer/token/balance_reader.ex @@ -27,7 +27,7 @@ defmodule Explorer.Token.BalanceReader do } ] - @erc1155_balance_function_abi [ + @nft_balance_function_abi [ %{ "constant" => true, "inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}], @@ -67,7 +67,7 @@ defmodule Explorer.Token.BalanceReader do ) :: [{:ok, non_neg_integer()} | {:error, String.t()}] def get_balances_of_with_abi(token_balance_requests, abi) do formatted_balances_requests = - if abi == @erc1155_balance_function_abi do + if abi == @nft_balance_function_abi do token_balance_requests |> Enum.map(&format_erc_1155_balance_request/1) else @@ -93,7 +93,7 @@ defmodule Explorer.Token.BalanceReader do } ]) :: [{:ok, non_neg_integer()} | {:error, String.t()}] def get_balances_of_erc_1155(token_balance_requests) do - get_balances_of_with_abi(token_balance_requests, @erc1155_balance_function_abi) + get_balances_of_with_abi(token_balance_requests, @nft_balance_function_abi) end defp format_balance_request(%{ diff --git a/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex b/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex index 7b0a092245f0..01bc78d39e28 100644 --- a/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex +++ b/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex @@ -34,38 +34,45 @@ defmodule Explorer.TokenInstanceOwnerAddressMigration.Helper do | {:error, any, any, map} def fetch_and_insert(batch) do changes = - Enum.map(batch, fn %{token_id: token_id, token_contract_address_hash: token_contract_address_hash} -> - token_transfer_query = - from(tt in TokenTransfer.only_consensus_transfers_query(), - where: - tt.token_contract_address_hash == ^token_contract_address_hash and - fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^token_id), - order_by: [desc: tt.block_number, desc: tt.log_index], - limit: 1, - select: %{ - token_contract_address_hash: tt.token_contract_address_hash, - token_ids: tt.token_ids, - to_address_hash: tt.to_address_hash, - block_number: tt.block_number, - log_index: tt.log_index - } - ) + batch + |> Enum.map(&process_instance/1) + |> Enum.reject(&is_nil/1) - token_transfer = - Repo.one(token_transfer_query) || - %{to_address_hash: @burn_address_hash, block_number: -1, log_index: -1} + Chain.import(%{token_instances: %{params: changes}}) + end - %{ - token_contract_address_hash: token_contract_address_hash, - token_id: token_id, - token_type: "ERC-721", - owner_address_hash: token_transfer.to_address_hash, - owner_updated_at_block: token_transfer.block_number, - owner_updated_at_log_index: token_transfer.log_index + defp process_instance(%{token_id: token_id, token_contract_address_hash: token_contract_address_hash}) do + token_transfer_query = + from(tt in TokenTransfer.only_consensus_transfers_query(), + where: + tt.token_contract_address_hash == ^token_contract_address_hash and + fragment("? @> ARRAY[?::decimal]", tt.token_ids, ^token_id), + order_by: [desc: tt.block_number, desc: tt.log_index], + limit: 1, + select: %{ + token_contract_address_hash: tt.token_contract_address_hash, + token_ids: tt.token_ids, + to_address_hash: tt.to_address_hash, + block_number: tt.block_number, + log_index: tt.log_index } - end) + ) - Chain.import(%{token_instances: %{params: changes}}) + token_transfer = + Repo.one(token_transfer_query, timeout: :timer.minutes(5)) || + %{to_address_hash: @burn_address_hash, block_number: -1, log_index: -1} + + %{ + token_contract_address_hash: token_contract_address_hash, + token_id: token_id, + token_type: "ERC-721", + owner_address_hash: token_transfer.to_address_hash, + owner_updated_at_block: token_transfer.block_number, + owner_updated_at_log_index: token_transfer.log_index + } + rescue + DBConnection.ConnectionError -> + nil end @spec unfilled_token_instances_exists? :: boolean diff --git a/apps/explorer/lib/explorer/token_instance_owner_address_migration/supervisor.ex b/apps/explorer/lib/explorer/token_instance_owner_address_migration/supervisor.ex index 5bb8532fdf5a..01ca324f5f2d 100644 --- a/apps/explorer/lib/explorer/token_instance_owner_address_migration/supervisor.ex +++ b/apps/explorer/lib/explorer/token_instance_owner_address_migration/supervisor.ex @@ -5,7 +5,7 @@ defmodule Explorer.TokenInstanceOwnerAddressMigration.Supervisor do use Supervisor - alias Explorer.TokenInstanceOwnerAddressMigration.{Helper, Worker} + alias Explorer.TokenInstanceOwnerAddressMigration.Worker def start_link(init_arg) do Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) @@ -13,9 +13,11 @@ defmodule Explorer.TokenInstanceOwnerAddressMigration.Supervisor do @impl true def init(_init_arg) do - if Helper.unfilled_token_instances_exists?() do + params = Application.get_env(:explorer, Explorer.TokenInstanceOwnerAddressMigration) + + if params[:enabled] do children = [ - {Worker, Application.get_env(:explorer, Explorer.TokenInstanceOwnerAddressMigration)} + {Worker, params} ] Supervisor.init(children, strategy: :one_for_one) diff --git a/apps/explorer/lib/explorer/token_instance_owner_address_migration/worker.ex b/apps/explorer/lib/explorer/token_instance_owner_address_migration/worker.ex index c0ad7c487997..c9cfabc9d2ca 100644 --- a/apps/explorer/lib/explorer/token_instance_owner_address_migration/worker.ex +++ b/apps/explorer/lib/explorer/token_instance_owner_address_migration/worker.ex @@ -14,7 +14,7 @@ defmodule Explorer.TokenInstanceOwnerAddressMigration.Worker do alias Explorer.Repo alias Explorer.TokenInstanceOwnerAddressMigration.Helper - def start_link(concurrency: concurrency, batch_size: batch_size) do + def start_link(concurrency: concurrency, batch_size: batch_size, enabled: _) do GenServer.start_link(__MODULE__, %{concurrency: concurrency, batch_size: batch_size}, name: __MODULE__) end diff --git a/apps/explorer/lib/explorer/token_transfer_token_id_migration/lowest_block_number_updater.ex b/apps/explorer/lib/explorer/token_transfer_token_id_migration/lowest_block_number_updater.ex deleted file mode 100644 index 5794e524ccfc..000000000000 --- a/apps/explorer/lib/explorer/token_transfer_token_id_migration/lowest_block_number_updater.ex +++ /dev/null @@ -1,65 +0,0 @@ -defmodule Explorer.TokenTransferTokenIdMigration.LowestBlockNumberUpdater do - @moduledoc """ - Collects processed block numbers from token id migration workers - and updates last_processed_block_number according to them. - Full algorithm is in the 'Indexer.Fetcher.TokenTransferTokenIdMigration.Supervisor' module doc. - """ - use GenServer - - alias Explorer.Utility.TokenTransferTokenIdMigratorProgress - - def start_link(_) do - GenServer.start_link(__MODULE__, :ok, name: __MODULE__) - end - - @impl true - def init(_) do - last_processed_block_number = TokenTransferTokenIdMigratorProgress.get_last_processed_block_number() - - {:ok, %{last_processed_block_number: last_processed_block_number, processed_ranges: []}} - end - - def add_range(from, to) do - GenServer.cast(__MODULE__, {:add_range, from..to}) - end - - @impl true - def handle_cast({:add_range, range}, %{processed_ranges: processed_ranges} = state) do - ranges = - [range | processed_ranges] - |> Enum.sort_by(& &1.last, &>=/2) - |> normalize_ranges() - - {new_last_number, new_ranges} = maybe_update_last_processed_number(state.last_processed_block_number, ranges) - - {:noreply, %{last_processed_block_number: new_last_number, processed_ranges: new_ranges}} - end - - defp normalize_ranges(ranges) do - %{prev_range: prev, result: result} = - Enum.reduce(ranges, %{prev_range: nil, result: []}, fn range, %{prev_range: prev_range, result: result} -> - case {prev_range, range} do - {nil, _} -> - %{prev_range: range, result: result} - - {%{last: l1} = r1, %{first: f2} = r2} when l1 - 1 > f2 -> - %{prev_range: r2, result: [r1 | result]} - - {%{first: f1}, %{last: l2}} -> - %{prev_range: f1..l2, result: result} - end - end) - - Enum.reverse([prev | result]) - end - - # since ranges are normalized, we need to check only the first range to determine the new last_processed_number - defp maybe_update_last_processed_number(current_last, [from..to | rest] = ranges) when current_last - 1 <= from do - case TokenTransferTokenIdMigratorProgress.update_last_processed_block_number(to) do - {:ok, _} -> {to, rest} - _ -> {current_last, ranges} - end - end - - defp maybe_update_last_processed_number(current_last, ranges), do: {current_last, ranges} -end diff --git a/apps/explorer/lib/explorer/token_transfer_token_id_migration/supervisor.ex b/apps/explorer/lib/explorer/token_transfer_token_id_migration/supervisor.ex deleted file mode 100644 index 2121158fee35..000000000000 --- a/apps/explorer/lib/explorer/token_transfer_token_id_migration/supervisor.ex +++ /dev/null @@ -1,70 +0,0 @@ -defmodule Explorer.TokenTransferTokenIdMigration.Supervisor do - @moduledoc """ - Supervises parts of token id migration process. - - Migration process algorithm: - - Defining the bounds of migration (by the first and the last block number of TokenTransfer). - Supervisor starts the workers in amount equal to 'TOKEN_ID_MIGRATION_CONCURRENCY' env value (defaults to 1) - and the 'LowestBlockNumberUpdater'. - - Each worker goes through the token transfers by batches ('TOKEN_ID_MIGRATION_BATCH_SIZE', defaults to 500) - and updates the token_ids to be equal of [token_id] for transfers that has any token_id. - Worker goes from the newest blocks to latest. - After worker is done with current batch, it sends the information about processed batch to 'LowestBlockNumberUpdater' - and takes the next by defining its bounds based on amount of all workers. - - For example, if batch size is 10, we have 5 workers and 100 items to be processed, - the distribution will be like this: - 1 worker - 99..90, 49..40 - 2 worker - 89..80, 39..30 - 3 worker - 79..70, 29..20 - 4 worker - 69..60, 19..10 - 5 worker - 59..50, 9..0 - - 'LowestBlockNumberUpdater' keeps the information about the last processed block number - (which is stored in the 'token_transfer_token_id_migrator_progress' db entity) - and block ranges that has already been processed by the workers but couldn't be committed - to last processed block number yet (because of the possible gap between the current last block - and upper bound of the last processed batch). Uncommitted block numbers are stored in normalize ranges. - When there is no gap between the last processed block number and the upper bound of the upper range, - 'LowestBlockNumberUpdater' updates the last processed block number in db and drops this range from its state. - - This supervisor won't start if the migration is completed - (last processed block number in db == 'TOKEN_ID_MIGRATION_FIRST_BLOCK' (defaults to 0)). - """ - use Supervisor - - alias Explorer.TokenTransferTokenIdMigration.{LowestBlockNumberUpdater, Worker} - alias Explorer.Utility.TokenTransferTokenIdMigratorProgress - - @default_first_block 0 - @default_workers_count 1 - - def start_link(_) do - Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) - end - - @impl true - def init(_) do - first_block = Application.get_env(:explorer, :token_id_migration)[:first_block] || @default_first_block - last_block = TokenTransferTokenIdMigratorProgress.get_last_processed_block_number() - - if last_block > first_block do - workers_count = Application.get_env(:explorer, :token_id_migration)[:concurrency] || @default_workers_count - - workers = - Enum.map(1..workers_count, fn id -> - Supervisor.child_spec( - {Worker, idx: id, first_block: first_block, last_block: last_block, step: workers_count - 1}, - id: {Worker, id}, - restart: :transient - ) - end) - - Supervisor.init([LowestBlockNumberUpdater | workers], strategy: :one_for_one) - else - :ignore - end - end -end diff --git a/apps/explorer/lib/explorer/token_transfer_token_id_migration/worker.ex b/apps/explorer/lib/explorer/token_transfer_token_id_migration/worker.ex deleted file mode 100644 index f7916ed0582a..000000000000 --- a/apps/explorer/lib/explorer/token_transfer_token_id_migration/worker.ex +++ /dev/null @@ -1,84 +0,0 @@ -defmodule Explorer.TokenTransferTokenIdMigration.Worker do - @moduledoc """ - Performs the migration of TokenTransfer token_id to token_ids by batches. - Full algorithm is in the 'Explorer.TokenTransferTokenIdMigration.Supervisor' module doc. - """ - use GenServer - - import Ecto.Query - - alias Explorer.Chain.TokenTransfer - alias Explorer.Repo - alias Explorer.TokenTransferTokenIdMigration.LowestBlockNumberUpdater - - @default_batch_size 500 - @interval 10 - - def start_link(idx: idx, first_block: first, last_block: last, step: step) do - GenServer.start_link(__MODULE__, %{idx: idx, bottom_block: first, last_block: last, step: step}) - end - - @impl true - def init(%{idx: idx, bottom_block: bottom_block, last_block: last_block, step: step}) do - batch_size = Application.get_env(:explorer, :token_id_migration)[:batch_size] || @default_batch_size - range = calculate_new_range(last_block, bottom_block, batch_size, idx - 1) - - schedule_next_update() - - {:ok, %{batch_size: batch_size, bottom_block: bottom_block, step: step, current_range: range}} - end - - @impl true - def handle_info(:update, %{current_range: :out_of_bound} = state) do - {:stop, :normal, state} - end - - @impl true - def handle_info(:update, %{current_range: {lower_bound, upper_bound}} = state) do - case do_update(lower_bound, upper_bound) do - true -> - LowestBlockNumberUpdater.add_range(upper_bound, lower_bound) - new_range = calculate_new_range(lower_bound, state.bottom_block, state.batch_size, state.step) - schedule_next_update() - {:noreply, %{state | current_range: new_range}} - - _ -> - schedule_next_update() - {:noreply, state} - end - end - - defp calculate_new_range(last_processed_block, bottom_block, batch_size, step) do - upper_bound = last_processed_block - step * batch_size - 1 - lower_bound = max(upper_bound - batch_size + 1, bottom_block) - - if upper_bound >= bottom_block do - {lower_bound, upper_bound} - else - :out_of_bound - end - end - - defp do_update(lower_bound, upper_bound) do - token_transfers_batch_query = - from( - tt in TokenTransfer, - where: tt.block_number >= ^lower_bound, - where: tt.block_number <= ^upper_bound - ) - - token_transfers_batch_query - |> Repo.all() - |> Enum.filter(fn %{token_id: token_id} -> not is_nil(token_id) end) - |> Enum.map(fn token_transfer -> - token_transfer - |> TokenTransfer.changeset(%{token_ids: [token_transfer.token_id], token_id: nil}) - |> Repo.update() - end) - |> Enum.all?(&match?({:ok, _}, &1)) - end - - defp schedule_next_update do - Process.send_after(self(), :update, @interval) - end -end diff --git a/apps/explorer/lib/explorer/utility/event_notification.ex b/apps/explorer/lib/explorer/utility/event_notification.ex index 8ae9dd0a2f5f..ccb52ea5e9b0 100644 --- a/apps/explorer/lib/explorer/utility/event_notification.ex +++ b/apps/explorer/lib/explorer/utility/event_notification.ex @@ -5,7 +5,7 @@ defmodule Explorer.Utility.EventNotification do use Explorer.Schema - schema "event_notifications" do + typed_schema "event_notifications" do field(:data, :string) end diff --git a/apps/explorer/lib/explorer/utility/massive_block.ex b/apps/explorer/lib/explorer/utility/massive_block.ex new file mode 100644 index 000000000000..9aa710480396 --- /dev/null +++ b/apps/explorer/lib/explorer/utility/massive_block.ex @@ -0,0 +1,42 @@ +defmodule Explorer.Utility.MassiveBlock do + @moduledoc """ + Module is responsible for keeping the block numbers that are too large for regular import + and need more time to complete. + """ + + use Explorer.Schema + + alias Explorer.Repo + + @primary_key false + typed_schema "massive_blocks" do + field(:number, :integer, primary_key: true) + + timestamps() + end + + @doc false + def changeset(massive_block \\ %__MODULE__{}, params) do + cast(massive_block, params, [:number]) + end + + def get_last_block_number(except_numbers) do + __MODULE__ + |> where([mb], mb.number not in ^except_numbers) + |> select([mb], max(mb.number)) + |> Repo.one() + end + + def insert_block_numbers(numbers) do + now = DateTime.utc_now() + params = Enum.map(numbers, &%{number: &1, inserted_at: now, updated_at: now}) + + Repo.insert_all(__MODULE__, params, on_conflict: {:replace, [:updated_at]}, conflict_target: :number) + end + + def delete_block_number(number) do + __MODULE__ + |> where([mb], mb.number == ^number) + |> Repo.delete_all() + end +end diff --git a/apps/explorer/lib/explorer/utility/microservice.ex b/apps/explorer/lib/explorer/utility/microservice.ex new file mode 100644 index 000000000000..ecdec3134e4b --- /dev/null +++ b/apps/explorer/lib/explorer/utility/microservice.ex @@ -0,0 +1,39 @@ +defmodule Explorer.Utility.Microservice do + @moduledoc """ + Module is responsible for common utils related to microservices. + """ + + alias Explorer.Helper + + @doc """ + Returns base url of the microservice or nil if it is invalid or not set + """ + @spec base_url(atom(), atom()) :: false | nil | binary() + def base_url(application \\ :explorer, module) do + url = Application.get_env(application, module)[:service_url] + + cond do + not Helper.valid_url?(url) -> + nil + + String.ends_with?(url, "/") -> + url + |> String.slice(0..(String.length(url) - 2)) + + true -> + url + end + end + + @doc """ + Returns :ok if Application.get_env(:explorer, module)[:enabled] is true (module is enabled) + """ + @spec check_enabled(atom) :: :ok | {:error, :disabled} + def check_enabled(application \\ :explorer, module) do + if Application.get_env(application, module)[:enabled] && base_url(application, module) do + :ok + else + {:error, :disabled} + end + end +end diff --git a/apps/explorer/lib/explorer/utility/missing_block_range.ex b/apps/explorer/lib/explorer/utility/missing_block_range.ex index 4892334d5fd5..1bfd30b34044 100644 --- a/apps/explorer/lib/explorer/utility/missing_block_range.ex +++ b/apps/explorer/lib/explorer/utility/missing_block_range.ex @@ -4,11 +4,12 @@ defmodule Explorer.Utility.MissingBlockRange do """ use Explorer.Schema + alias Explorer.Chain.BlockNumberHelper alias Explorer.Repo @default_returning_batch_size 10 - schema "missing_block_ranges" do + typed_schema "missing_block_ranges" do field(:from_number, :integer) field(:to_number, :integer) end @@ -76,21 +77,29 @@ defmodule Explorer.Utility.MissingBlockRange do case {lower_range, higher_range} do {%__MODULE__{} = same_range, %__MODULE__{} = same_range} -> Repo.delete(same_range) - insert_if_needed(%{from_number: same_range.from_number, to_number: max_number + 1}) - insert_if_needed(%{from_number: min_number - 1, to_number: same_range.to_number}) + + insert_if_needed(%{ + from_number: same_range.from_number, + to_number: BlockNumberHelper.next_block_number(max_number) + }) + + insert_if_needed(%{ + from_number: BlockNumberHelper.previous_block_number(min_number), + to_number: same_range.to_number + }) {%__MODULE__{} = range, nil} -> delete_ranges_between(max_number, range.from_number) - update_from_number_or_delete_range(range, min_number - 1) + update_from_number_or_delete_range(range, BlockNumberHelper.previous_block_number(min_number)) {nil, %__MODULE__{} = range} -> delete_ranges_between(range.to_number, min_number) - update_to_number_or_delete_range(range, max_number + 1) + update_to_number_or_delete_range(range, BlockNumberHelper.next_block_number(max_number)) {%__MODULE__{} = range_1, %__MODULE__{} = range_2} -> delete_ranges_between(range_2.to_number, range_1.from_number) - update_from_number_or_delete_range(range_1, min_number - 1) - update_to_number_or_delete_range(range_2, max_number + 1) + update_from_number_or_delete_range(range_1, BlockNumberHelper.previous_block_number(min_number)) + update_to_number_or_delete_range(range_2, BlockNumberHelper.next_block_number(max_number)) _ -> delete_ranges_between(max_number, min_number) @@ -145,7 +154,7 @@ defmodule Explorer.Utility.MissingBlockRange do __MODULE__ |> where([r], r.from_number < r.to_number) |> update([r], set: [from_number: r.to_number, to_number: r.from_number]) - |> Repo.update_all([]) + |> Repo.update_all([], timeout: :infinity) {last_range, merged_ranges} = delete_and_merge_ranges() @@ -175,7 +184,7 @@ defmodule Explorer.Utility.MissingBlockRange do (r.to_number <= r1.from_number and r.to_number >= r1.to_number)) and r1.id != r.id ) |> select([r, r1], r) - |> Repo.delete_all() + |> Repo.delete_all(timeout: :infinity) intersecting_ranges end diff --git a/apps/explorer/lib/explorer/utility/rust_service.ex b/apps/explorer/lib/explorer/utility/rust_service.ex deleted file mode 100644 index 63f949961252..000000000000 --- a/apps/explorer/lib/explorer/utility/rust_service.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Explorer.Utility.RustService do - @moduledoc """ - Module is responsible for common utils related to rust microservices. - """ - def base_url(module) do - url = Application.get_env(:explorer, module)[:service_url] - - if String.ends_with?(url, "/") do - url - |> String.slice(0..(String.length(url) - 2)) - else - url - end - end -end diff --git a/apps/explorer/lib/explorer/utility/token_transfer_token_id_migrator_progress.ex b/apps/explorer/lib/explorer/utility/token_transfer_token_id_migrator_progress.ex deleted file mode 100644 index 3a28181016e5..000000000000 --- a/apps/explorer/lib/explorer/utility/token_transfer_token_id_migrator_progress.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Explorer.Utility.TokenTransferTokenIdMigratorProgress do - @moduledoc """ - Module is responsible for keeping the current progress of TokenTransfer token_id migration. - Full algorithm is in the 'Indexer.Fetcher.TokenTransferTokenIdMigration.Supervisor' module doc. - """ - use Explorer.Schema - - require Logger - - alias Explorer.Chain.Cache.BlockNumber - alias Explorer.Repo - - schema "token_transfer_token_id_migrator_progress" do - field(:last_processed_block_number, :integer) - - timestamps() - end - - @doc false - def changeset(progress \\ %__MODULE__{}, params) do - cast(progress, params, [:last_processed_block_number]) - end - - def get_current_progress do - Repo.one( - from( - p in __MODULE__, - order_by: [desc: p.updated_at], - limit: 1 - ) - ) - end - - def get_last_processed_block_number do - case get_current_progress() do - nil -> - latest_processed_block_number = BlockNumber.get_max() + 1 - update_last_processed_block_number(latest_processed_block_number) - latest_processed_block_number - - %{last_processed_block_number: block_number} -> - block_number - end - end - - def update_last_processed_block_number(block_number, force \\ false) do - case get_current_progress() do - nil -> - %{last_processed_block_number: block_number} - |> changeset() - |> Repo.insert() - - progress -> - if not force and progress.last_processed_block_number < block_number do - Logger.error( - "TokenTransferTokenIdMigratorProgress new block_number is above the last one. Last: #{progress.last_processed_block_number}, new: #{block_number}" - ) - - {:error, :invalid_block_number} - else - progress - |> changeset(%{last_processed_block_number: block_number}) - |> Repo.update() - end - end - end -end diff --git a/apps/explorer/lib/explorer/visualize/sol2uml.ex b/apps/explorer/lib/explorer/visualize/sol2uml.ex index 10a6c2a9efda..459531a36299 100644 --- a/apps/explorer/lib/explorer/visualize/sol2uml.ex +++ b/apps/explorer/lib/explorer/visualize/sol2uml.ex @@ -2,7 +2,7 @@ defmodule Explorer.Visualize.Sol2uml do @moduledoc """ Adapter for sol2uml visualizer with https://github.com/blockscout/blockscout-rs/blob/main/visualizer """ - alias Explorer.Utility.RustService + alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger @@ -61,7 +61,7 @@ defmodule Explorer.Visualize.Sol2uml do def base_api_url, do: "#{base_url()}" <> "/api/v1" def base_url do - RustService.base_url(__MODULE__) + Microservice.base_url(__MODULE__) end def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index b5f5d16db9a2..c8df548f7ac6 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -11,7 +11,7 @@ defmodule Explorer.Mixfile do deps_path: "../../deps", description: "Read-access to indexed block chain data.", dialyzer: [ - plt_add_deps: :transitive, + plt_add_deps: :app_tree, plt_add_apps: ~w(ex_unit mix)a, ignore_warnings: "../../.dialyzer-ignore" ], @@ -24,8 +24,8 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "5.3.1", - xref: [exclude: [BlockScoutWeb.WebRouter.Helpers]] + version: "6.3.0", + xref: [exclude: [BlockScoutWeb.WebRouter.Helpers, Indexer.Helper]] ] end @@ -61,7 +61,7 @@ defmodule Explorer.Mixfile do {:mime, "~> 2.0"}, {:bcrypt_elixir, "~> 3.0"}, # benchmark optimizations - {:benchee, "~> 1.1.0", only: :test}, + {:benchee, "~> 1.3.0", only: :test}, # CSV output for benchee {:benchee_csv, "~> 1.0.0", only: :test}, {:bypass, "~> 2.1", only: :test}, @@ -117,7 +117,9 @@ defmodule Explorer.Mixfile do {:cbor, "~> 1.0"}, {:cloak_ecto, "~> 1.2.0"}, {:redix, "~> 1.1"}, - {:hammer_backend_redis, "~> 6.1"} + {:hammer_backend_redis, "~> 6.1"}, + {:logger_json, "~> 5.1"}, + {:typed_ecto_schema, "~> 0.4.1", runtime: false} ] end diff --git a/apps/explorer/package-lock.json b/apps/explorer/package-lock.json index 9a78d3dd8dde..6d5940bb3258 100644 --- a/apps/explorer/package-lock.json +++ b/apps/explorer/package-lock.json @@ -7,7 +7,7 @@ "name": "blockscout", "license": "GPL-3.0", "dependencies": { - "solc": "0.8.22" + "solc": "0.8.24" }, "engines": { "node": "18.x", @@ -28,9 +28,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -76,9 +76,9 @@ } }, "node_modules/solc": { - "version": "0.8.22", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.22.tgz", - "integrity": "sha512-bA2tMZXx93R8L5LUH7TlB/f+QhkVyxrrY6LmgJnFFZlRknrhYVlBK1e3uHIdKybwoFabOFSzeaZjPeL/GIpFGQ==", + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.24.tgz", + "integrity": "sha512-G5yUqjTUPc8Np74sCFwfsevhBPlUifUOfhYrgyu6CmYlC6feSw0YS6eZW47XDT23k3JYdKx5nJ+Q7whCEmNcoA==", "dependencies": { "command-exists": "^1.2.8", "commander": "^8.1.0", @@ -119,9 +119,9 @@ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" }, "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" }, "js-sha3": { "version": "0.8.0", @@ -144,9 +144,9 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "solc": { - "version": "0.8.22", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.22.tgz", - "integrity": "sha512-bA2tMZXx93R8L5LUH7TlB/f+QhkVyxrrY6LmgJnFFZlRknrhYVlBK1e3uHIdKybwoFabOFSzeaZjPeL/GIpFGQ==", + "version": "0.8.24", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.24.tgz", + "integrity": "sha512-G5yUqjTUPc8Np74sCFwfsevhBPlUifUOfhYrgyu6CmYlC6feSw0YS6eZW47XDT23k3JYdKx5nJ+Q7whCEmNcoA==", "requires": { "command-exists": "^1.2.8", "commander": "^8.1.0", diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 366fdd199b1e..47db342df7aa 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -13,6 +13,6 @@ }, "scripts": {}, "dependencies": { - "solc": "0.8.22" + "solc": "0.8.24" } } diff --git a/apps/explorer/priv/account/migrations/20231207201701_add_watchlist_id_column.exs b/apps/explorer/priv/account/migrations/20231207201701_add_watchlist_id_column.exs new file mode 100644 index 000000000000..346c9ec05ee8 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20231207201701_add_watchlist_id_column.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Account.Migrations.AddWatchlistIdColumn do + use Ecto.Migration + + def change do + execute(""" + ALTER TABLE public.account_watchlist_notifications + DROP CONSTRAINT account_watchlist_notifications_watchlist_address_id_fkey; + """) + + alter table(:account_watchlist_notifications) do + add(:watchlist_id, :bigserial) + end + + create(index(:account_watchlist_notifications, [:watchlist_id])) + + execute(""" + UPDATE account_watchlist_notifications awn + SET watchlist_id = awa.watchlist_id + FROM account_watchlist_addresses awa + WHERE awa.id = awn.watchlist_address_id + """) + end +end diff --git a/apps/explorer/priv/account/migrations/20240219152220_add_account_watchlist_addresses_erc_404_fields.exs b/apps/explorer/priv/account/migrations/20240219152220_add_account_watchlist_addresses_erc_404_fields.exs new file mode 100644 index 000000000000..7fbb08a48cd4 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20240219152220_add_account_watchlist_addresses_erc_404_fields.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Account.Migrations.AddAccountWatchlistAddressesErc404Fields do + use Ecto.Migration + + def change do + alter table(:account_watchlist_addresses) do + add(:watch_erc_404_input, :boolean, default: true) + add(:watch_erc_404_output, :boolean, default: true) + end + end +end diff --git a/apps/explorer/priv/beacon/migrations/20240109102458_create_blobs_tables.exs b/apps/explorer/priv/beacon/migrations/20240109102458_create_blobs_tables.exs new file mode 100644 index 000000000000..e67cf501babc --- /dev/null +++ b/apps/explorer/priv/beacon/migrations/20240109102458_create_blobs_tables.exs @@ -0,0 +1,34 @@ +defmodule Explorer.Repo.Beacon.Migrations.CreateBlobsTables do + use Ecto.Migration + + def change do + create table(:beacon_blobs_transactions, primary_key: false) do + add(:hash, references(:transactions, column: :hash, on_delete: :delete_all, type: :bytea), + null: false, + primary_key: true + ) + + add(:max_fee_per_blob_gas, :numeric, precision: 100, null: false) + add(:blob_gas_price, :numeric, precision: 100, null: false) + add(:blob_gas_used, :numeric, precision: 100, null: false) + add(:blob_versioned_hashes, {:array, :bytea}, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + + alter table(:blocks) do + add(:blob_gas_used, :numeric, precision: 100) + add(:excess_blob_gas, :numeric, precision: 100) + end + + create table(:beacon_blobs, primary_key: false) do + add(:hash, :bytea, null: false, primary_key: true) + + add(:blob_data, :bytea, null: true) + add(:kzg_commitment, :bytea, null: true) + add(:kzg_proof, :bytea, null: true) + + timestamps(updated_at: false, null: false, type: :utc_datetime_usec, default: fragment("now()")) + end + end +end diff --git a/apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs b/apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs new file mode 100644 index 000000000000..2622358c1dff --- /dev/null +++ b/apps/explorer/priv/bridged_tokens/migrations/20230919080116_add_bridged_tokens.exs @@ -0,0 +1,29 @@ +defmodule Explorer.Repo.BridgedTokens.Migrations.AddBridgedTokens do + use Ecto.Migration + + def change do + alter table(:tokens) do + add(:bridged, :boolean, null: true) + end + + create table(:bridged_tokens, primary_key: false) do + add(:foreign_chain_id, :numeric, null: false) + add(:foreign_token_contract_address_hash, :bytea, null: false) + add(:exchange_rate, :decimal) + add(:custom_metadata, :string, null: true) + add(:lp_token, :boolean, null: true) + add(:custom_cap, :decimal, null: true) + add(:type, :string, null: true) + + add( + :home_token_contract_address_hash, + references(:tokens, column: :contract_address_hash, on_delete: :delete_all, type: :bytea), + null: false + ) + + timestamps() + end + + create(unique_index(:bridged_tokens, :home_token_contract_address_hash)) + end +end diff --git a/apps/explorer/priv/contracts_abi/posdao/BlockRewardAuRa.json b/apps/explorer/priv/contracts_abi/posdao/BlockRewardAuRa.json deleted file mode 100644 index 49737b7a3220..000000000000 --- a/apps/explorer/priv/contracts_abi/posdao/BlockRewardAuRa.json +++ /dev/null @@ -1,636 +0,0 @@ -[ - { - "constant": true, - "inputs": [ - { - "name": "_poolStakingAddress", - "type": "address" - }, - { - "name": "_staker", - "type": "address" - } - ], - "name": "epochsToClaimRewardFrom", - "outputs": [ - { - "name": "epochsToClaimFrom", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "bridgeTokenReward", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - }, - { - "name": "", - "type": "uint256" - } - ], - "name": "mintedForAccountInBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "uint256" - } - ], - "name": "epochPoolNativeReward", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "mintedForAccount", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "mintedInBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "mintedTotally", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "tokenRewardUndistributed", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "nativeRewardUndistributed", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "mintedTotallyByBridge", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "uint256" - } - ], - "name": "epochPoolTokenReward", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "validatorMinRewardPercent", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "validatorSetContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "bridgeNativeReward", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "uint256" - } - ], - "name": "blocksCreated", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "amount", - "type": "uint256" - }, - { - "indexed": true, - "name": "receiver", - "type": "address" - }, - { - "indexed": true, - "name": "bridge", - "type": "address" - } - ], - "name": "AddedReceiver", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "receivers", - "type": "address[]" - }, - { - "indexed": false, - "name": "rewards", - "type": "uint256[]" - } - ], - "name": "MintedNative", - "type": "event" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "addBridgeNativeRewardReceivers", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "addBridgeTokenRewardReceivers", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - }, - { - "name": "_receiver", - "type": "address" - } - ], - "name": "addExtraReceiver", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_validatorSet", - "type": "address" - }, - { - "name": "_prevBlockReward", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_bridge", - "type": "address" - }, - { - "name": "_prevBlockRewardContract", - "type": "address" - } - ], - "name": "migrateMintingStatistics", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "benefactors", - "type": "address[]" - }, - { - "name": "kind", - "type": "uint16[]" - } - ], - "name": "reward", - "outputs": [ - { - "name": "receiversNative", - "type": "address[]" - }, - { - "name": "rewardsNative", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_bridgesAllowed", - "type": "address[]" - } - ], - "name": "setErcToNativeBridgesAllowed", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_bridgesAllowed", - "type": "address[]" - } - ], - "name": "setNativeToErcBridgesAllowed", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_bridgesAllowed", - "type": "address[]" - } - ], - "name": "setErcToErcBridgesAllowed", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "blockRewardContractId", - "outputs": [ - { - "name": "", - "type": "bytes4" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "ercToErcBridgesAllowed", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "ercToNativeBridgesAllowed", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "extraReceiversQueueSize", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isInitialized", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "nativeToErcBridgesAllowed", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - } - ], - "name": "validatorRewardPercent", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_stakingEpoch", - "type": "uint256" - }, - { - "name": "_validatorStaked", - "type": "uint256" - }, - { - "name": "_totalStaked", - "type": "uint256" - }, - { - "name": "_poolReward", - "type": "uint256" - } - ], - "name": "validatorShare", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_stakingEpoch", - "type": "uint256" - }, - { - "name": "_delegatorStaked", - "type": "uint256" - }, - { - "name": "_validatorStaked", - "type": "uint256" - }, - { - "name": "_totalStaked", - "type": "uint256" - }, - { - "name": "_poolReward", - "type": "uint256" - } - ], - "name": "delegatorShare", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - } -] diff --git a/apps/explorer/priv/contracts_abi/posdao/README.md b/apps/explorer/priv/contracts_abi/posdao/README.md deleted file mode 100644 index 98a1dd21090c..000000000000 --- a/apps/explorer/priv/contracts_abi/posdao/README.md +++ /dev/null @@ -1 +0,0 @@ -ABIs are taken from compiled contract JSONs in the `build/` directory of https://github.com/poanetwork/posdao-contracts. diff --git a/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json b/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json deleted file mode 100644 index aebd900c42e9..000000000000 --- a/apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json +++ /dev/null @@ -1,1127 +0,0 @@ -[ - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "address" - } - ], - "name": "poolDelegatorIndex", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "candidateMinStake", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_delegator", - "type": "address" - }, - { - "name": "_offset", - "type": "uint256" - }, - { - "name": "_length", - "type": "uint256" - } - ], - "name": "getDelegatorPools", - "outputs": [ - { - "name": "result", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_delegator", - "type": "address" - } - ], - "name": "getDelegatorPoolsLength", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "poolInactiveIndex", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakingEpochStartBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakingEpoch", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "erc677TokenContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "address" - } - ], - "name": "poolDelegatorInactiveIndex", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakeWithdrawDisallowPeriod", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "address" - } - ], - "name": "orderWithdrawEpoch", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "poolIndex", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakingEpochDuration", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "delegatorMinStake", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "orderedWithdrawAmountTotal", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "validatorSetContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "address" - } - ], - "name": "orderedWithdrawAmount", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "poolToBeRemovedIndex", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "MAX_CANDIDATES", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "poolToBeElectedIndex", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "fromPoolId", - "type": "uint256" - } - ], - "name": "ClaimedOrderedWithdrawal", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "toPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "toPoolId", - "type": "uint256" - } - ], - "name": "PlacedStake", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "toPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "fromPoolId", - "type": "uint256" - }, - { - "indexed": false, - "name": "toPoolId", - "type": "uint256" - } - ], - "name": "MovedStake", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "int256" - }, - { - "indexed": false, - "name": "fromPoolId", - "type": "uint256" - } - ], - "name": "OrderedWithdrawal", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "name": "fromPoolId", - "type": "uint256" - } - ], - "name": "WithdrewStake", - "type": "event" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - }, - { - "name": "_miningAddress", - "type": "address" - } - ], - "name": "addPool", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_stakingEpochs", - "type": "uint256[]" - }, - { - "name": "_poolStakingAddress", - "type": "address" - } - ], - "name": "claimReward", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_unremovablePoolId", - "type": "uint256" - } - ], - "name": "clearUnremovableValidator", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "incrementStakingEpoch", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_validatorSetContract", - "type": "address" - }, - { - "name": "_initialIds", - "type": "uint256[]" - }, - { - "name": "_delegatorMinStake", - "type": "uint256" - }, - { - "name": "_candidateMinStake", - "type": "uint256" - }, - { - "name": "_stakingEpochDuration", - "type": "uint256" - }, - { - "name": "_stakingEpochStartBlock", - "type": "uint256" - }, - { - "name": "_stakeWithdrawDisallowPeriod", - "type": "uint256" - } - ], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - } - ], - "name": "removePool", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "removeMyPool", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_blockNumber", - "type": "uint256" - } - ], - "name": "setStakingEpochStartBlock", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_fromPoolStakingAddress", - "type": "address" - }, - { - "name": "_toPoolStakingAddress", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "moveStake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_toPoolStakingAddress", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "stake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_fromPoolStakingAddress", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "withdraw", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_poolStakingAddress", - "type": "address" - }, - { - "name": "_amount", - "type": "int256" - } - ], - "name": "orderWithdraw", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_poolStakingAddress", - "type": "address" - } - ], - "name": "claimOrderedWithdraw", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_erc677TokenContract", - "type": "address" - } - ], - "name": "setErc677TokenContract", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_minStake", - "type": "uint256" - } - ], - "name": "setCandidateMinStake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_minStake", - "type": "uint256" - } - ], - "name": "setDelegatorMinStake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPools", - "outputs": [ - { - "name": "", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPoolsInactive", - "outputs": [ - { - "name": "", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPoolsLikelihood", - "outputs": [ - { - "name": "likelihoods", - "type": "uint256[]" - }, - { - "name": "sum", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPoolsToBeElected", - "outputs": [ - { - "name": "", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPoolsToBeRemoved", - "outputs": [ - { - "name": "", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "areStakeAndWithdrawAllowed", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isInitialized", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - } - ], - "name": "isPoolActive", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolStakingAddress", - "type": "address" - }, - { - "name": "_staker", - "type": "address" - } - ], - "name": "maxWithdrawAllowed", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolStakingAddress", - "type": "address" - }, - { - "name": "_staker", - "type": "address" - } - ], - "name": "maxWithdrawOrderAllowed", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "", - "type": "address" - }, - { - "name": "", - "type": "uint256" - }, - { - "name": "", - "type": "bytes" - } - ], - "name": "onTokenTransfer", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - } - ], - "name": "poolDelegators", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - } - ], - "name": "poolDelegatorsInactive", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - }, - { - "name": "_delegatorOrZero", - "type": "address" - } - ], - "name": "stakeAmountByCurrentEpoch", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - }, - { - "name": "_delegatorOrZero", - "type": "address" - } - ], - "name": "stakeAmount", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_poolId", - "type": "uint256" - } - ], - "name": "stakeAmountTotal", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakingEpochEndBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "lastChangeBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - } -] diff --git a/apps/explorer/priv/contracts_abi/posdao/Token.json b/apps/explorer/priv/contracts_abi/posdao/Token.json deleted file mode 100644 index a10faa963858..000000000000 --- a/apps/explorer/priv/contracts_abi/posdao/Token.json +++ /dev/null @@ -1,968 +0,0 @@ -[ - { - "constant": false, - "inputs": [ - { - "name": "_bridge", - "type": "address" - } - ], - "name": "removeBridge", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_spender", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "PERMIT_TYPEHASH", - "outputs": [ - { - "name": "", - "type": "bytes32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "decimals", - "outputs": [ - { - "name": "", - "type": "uint8" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "DOMAIN_SEPARATOR", - "outputs": [ - { - "name": "", - "type": "bytes32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "spender", - "type": "address" - }, - { - "name": "addedValue", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_to", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - }, - { - "name": "_data", - "type": "bytes" - } - ], - "name": "transferAndCall", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_to", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "mint", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_value", - "type": "uint256" - } - ], - "name": "burn", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "bridgePointers", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "version", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "blockRewardContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_spender", - "type": "address" - }, - { - "name": "_subtractedValue", - "type": "uint256" - } - ], - "name": "decreaseApproval", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_token", - "type": "address" - }, - { - "name": "_to", - "type": "address" - } - ], - "name": "claimTokens", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_address", - "type": "address" - } - ], - "name": "isBridge", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "nonces", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getTokenInterfacesVersion", - "outputs": [ - { - "name": "major", - "type": "uint64" - }, - { - "name": "minor", - "type": "uint64" - }, - { - "name": "patch", - "type": "uint64" - } - ], - "payable": false, - "stateMutability": "pure", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_holder", - "type": "address" - }, - { - "name": "_spender", - "type": "address" - }, - { - "name": "_nonce", - "type": "uint256" - }, - { - "name": "_expiry", - "type": "uint256" - }, - { - "name": "_allowed", - "type": "bool" - }, - { - "name": "_v", - "type": "uint8" - }, - { - "name": "_r", - "type": "bytes32" - }, - { - "name": "_s", - "type": "bytes32" - } - ], - "name": "permit", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_bridge", - "type": "address" - } - ], - "name": "addBridge", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "bridgeList", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "spender", - "type": "address" - }, - { - "name": "subtractedValue", - "type": "uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_to", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "push", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_from", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "move", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "F_ADDR", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_spender", - "type": "address" - }, - { - "name": "_addedValue", - "type": "uint256" - } - ], - "name": "increaseApproval", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_owner", - "type": "address" - }, - { - "name": "_spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakingContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_from", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "pull", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "bridgeCount", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - }, - { - "name": "", - "type": "address" - } - ], - "name": "expirations", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "_name", - "type": "string" - }, - { - "name": "_symbol", - "type": "string" - }, - { - "name": "_decimals", - "type": "uint8" - }, - { - "name": "_chainId", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "bridge", - "type": "address" - } - ], - "name": "BridgeAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "bridge", - "type": "address" - } - ], - "name": "BridgeRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "from", - "type": "address" - }, - { - "indexed": false, - "name": "to", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "ContractFallbackCallFailed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "to", - "type": "address" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" - } - ], - "name": "Mint", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "burner", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Burn", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "from", - "type": "address" - }, - { - "indexed": true, - "name": "to", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - }, - { - "indexed": false, - "name": "data", - "type": "bytes" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "from", - "type": "address" - }, - { - "indexed": true, - "name": "to", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "constant": false, - "inputs": [ - { - "name": "_blockRewardContract", - "type": "address" - } - ], - "name": "setBlockRewardContract", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_stakingContract", - "type": "address" - } - ], - "name": "setStakingContract", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "mintReward", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_staker", - "type": "address" - }, - { - "name": "_amount", - "type": "uint256" - } - ], - "name": "stake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_to", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_from", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "_value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } -] diff --git a/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json b/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json deleted file mode 100644 index ca4c9e787f32..000000000000 --- a/apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json +++ /dev/null @@ -1,734 +0,0 @@ -[ - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "miningByStakingAddress", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "initiateChangeAllowed", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "banCounter", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "stakingByMiningAddress", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "blockRewardContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "banReason", - "outputs": [ - { - "name": "", - "type": "bytes32" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "bannedUntil", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "bannedDelegatorsUntil", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "unremovableValidator", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "MAX_VALIDATORS", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "validatorCounter", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "validatorSetApplyBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "randomContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "changeRequestCount", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "stakingContract", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "isValidator", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "parentHash", - "type": "bytes32" - }, - { - "indexed": false, - "name": "newSet", - "type": "address[]" - } - ], - "name": "InitiateChange", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "reportingValidator", - "type": "address" - }, - { - "indexed": false, - "name": "maliciousValidator", - "type": "address" - }, - { - "indexed": false, - "name": "blockNumber", - "type": "uint256" - } - ], - "name": "ReportedMalicious", - "type": "event" - }, - { - "constant": false, - "inputs": [], - "name": "clearUnremovableValidator", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "emitInitiateChange", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "finalizeChange", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_blockRewardContract", - "type": "address" - }, - { - "name": "_randomContract", - "type": "address" - }, - { - "name": "_stakingContract", - "type": "address" - }, - { - "name": "_initialMiningAddresses", - "type": "address[]" - }, - { - "name": "_initialStakingAddresses", - "type": "address[]" - }, - { - "name": "_firstValidatorIsUnremovable", - "type": "bool" - } - ], - "name": "initialize", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "newValidatorSet", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_miningAddresses", - "type": "address[]" - } - ], - "name": "removeMaliciousValidators", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_maliciousMiningAddress", - "type": "address" - }, - { - "name": "_blockNumber", - "type": "uint256" - }, - { - "name": "", - "type": "bytes" - } - ], - "name": "reportMalicious", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "emitInitiateChangeCallable", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPendingValidators", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "validatorsToBeFinalized", - "outputs": [ - { - "name": "miningAddresses", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getValidators", - "outputs": [ - { - "name": "", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "isInitialized", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_miningAddress", - "type": "address" - } - ], - "name": "isReportValidatorValid", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_miningAddress", - "type": "address" - } - ], - "name": "isValidatorBanned", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_miningAddress", - "type": "address" - } - ], - "name": "areDelegatorsBanned", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "_reportingMiningAddress", - "type": "address" - }, - { - "name": "_maliciousMiningAddress", - "type": "address" - }, - { - "name": "_blockNumber", - "type": "uint256" - } - ], - "name": "reportMaliciousCallable", - "outputs": [ - { - "name": "callable", - "type": "bool" - }, - { - "name": "removeReportingValidator", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "lastChangeBlock", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "hasEverBeenMiningAddress", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "idByMiningAddress", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "miningAddressById", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "stakingAddressById", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "address" - } - ], - "name": "idByStakingAddress", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "poolName", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "", - "type": "uint256" - } - ], - "name": "poolDescription", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_name", - "type": "string" - }, - { - "name": "_description", - "type": "string" - } - ], - "name": "changeMetadata", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } -] diff --git a/apps/explorer/priv/filecoin/migrations/20230731130103_modify_collated_gas_price_constraint.exs b/apps/explorer/priv/filecoin/migrations/20230731130103_modify_collated_gas_price_constraint.exs new file mode 100644 index 000000000000..18ff64b5f382 --- /dev/null +++ b/apps/explorer/priv/filecoin/migrations/20230731130103_modify_collated_gas_price_constraint.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Filecoin.Migrations.ModifyCollatedGasPriceConstraint do + use Ecto.Migration + + def change do + execute("ALTER TABLE transactions DROP CONSTRAINT collated_gas_price") + + create( + constraint( + :transactions, + :collated_gas_price, + check: "block_hash IS NULL OR gas_price IS NOT NULL OR max_fee_per_gas IS NOT NULL" + ) + ) + end +end diff --git a/apps/explorer/priv/filecoin/migrations/20231109104957_create_null_round_heights.exs b/apps/explorer/priv/filecoin/migrations/20231109104957_create_null_round_heights.exs new file mode 100644 index 000000000000..081d17637dab --- /dev/null +++ b/apps/explorer/priv/filecoin/migrations/20231109104957_create_null_round_heights.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Filecoin.Migrations.CreateNullRoundHeights do + use Ecto.Migration + + def change do + create table(:null_round_heights, primary_key: false) do + add(:height, :integer, primary_key: true) + end + end +end diff --git a/apps/explorer/priv/filecoin/migrations/20240219140124_change_null_round_heights_height_type.exs b/apps/explorer/priv/filecoin/migrations/20240219140124_change_null_round_heights_height_type.exs new file mode 100644 index 000000000000..590a313661a3 --- /dev/null +++ b/apps/explorer/priv/filecoin/migrations/20240219140124_change_null_round_heights_height_type.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Filecoin.Migrations.ChangeNullRoundHeightsHeightType do + use Ecto.Migration + + def change do + alter table(:null_round_heights) do + modify(:height, :bigint) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20220204060243_transaction_columns_to_support_l2.exs b/apps/explorer/priv/optimism/migrations/20220204060243_transaction_columns_to_support_l2.exs new file mode 100644 index 000000000000..36a7902cfdf5 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20220204060243_transaction_columns_to_support_l2.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Migrations.TransactionColumnsToSupportL2 do + use Ecto.Migration + + def change do + alter table(:transactions) do + add(:l1_fee, :numeric, precision: 100, null: true) + add(:l1_fee_scalar, :decimal, null: true) + add(:l1_gas_price, :numeric, precision: 100, null: true) + add(:l1_gas_used, :numeric, precision: 100, null: true) + add(:l1_tx_origin, :bytea, null: true) + add(:l1_block_number, :integer, null: true) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230131115105_add_op_output_roots_table.exs b/apps/explorer/priv/optimism/migrations/20230131115105_add_op_output_roots_table.exs new file mode 100644 index 000000000000..da07e936ee7d --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230131115105_add_op_output_roots_table.exs @@ -0,0 +1,16 @@ +defmodule Explorer.Repo.Migrations.AddOpOutputRootsTable do + use Ecto.Migration + + def change do + create table(:op_output_roots, primary_key: false) do + add(:l2_output_index, :bigint, null: false, primary_key: true) + add(:l2_block_number, :bigint, null: false) + add(:l1_tx_hash, :bytea, null: false) + add(:l1_timestamp, :"timestamp without time zone", null: false) + add(:l1_block_number, :bigint, null: false) + add(:output_root, :bytea, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230206123308_add_op_withdrawals_table.exs b/apps/explorer/priv/optimism/migrations/20230206123308_add_op_withdrawals_table.exs new file mode 100644 index 000000000000..cee87054f02b --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230206123308_add_op_withdrawals_table.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Migrations.AddOpWithdrawalsTable do + use Ecto.Migration + + def change do + create table(:op_withdrawals, primary_key: false) do + add(:msg_nonce, :numeric, precision: 100, null: false, primary_key: true) + add(:withdrawal_hash, :bytea, null: false) + add(:l2_tx_hash, :bytea, null: false) + add(:l2_block_number, :bigint, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230212162845_add_op_withdrawal_events_table.exs b/apps/explorer/priv/optimism/migrations/20230212162845_add_op_withdrawal_events_table.exs new file mode 100644 index 000000000000..12f5d3160614 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230212162845_add_op_withdrawal_events_table.exs @@ -0,0 +1,22 @@ +defmodule Explorer.Repo.Migrations.AddOpWithdrawalEventsTable do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE withdrawal_event_type AS ENUM ('WithdrawalProven', 'WithdrawalFinalized')", + "DROP TYPE withdrawal_event_type" + ) + + create table(:op_withdrawal_events, primary_key: false) do + add(:withdrawal_hash, :bytea, null: false, primary_key: true) + add(:l1_event_type, :withdrawal_event_type, null: false, primary_key: true) + add(:l1_timestamp, :"timestamp without time zone", null: false) + add(:l1_tx_hash, :bytea, null: false) + add(:l1_block_number, :bigint, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:op_withdrawal_events, :l1_timestamp)) + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230216135703_add_op_transaction_batches_table.exs b/apps/explorer/priv/optimism/migrations/20230216135703_add_op_transaction_batches_table.exs new file mode 100644 index 000000000000..59fbc9822675 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230216135703_add_op_transaction_batches_table.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Migrations.AddOpTransactionBatchesTable do + use Ecto.Migration + + def change do + create table(:op_transaction_batches, primary_key: false) do + add(:l2_block_number, :bigint, null: false, primary_key: true) + add(:epoch_number, :bigint, null: false) + add(:l1_tx_hashes, {:array, :bytea}, null: false) + add(:l1_tx_timestamp, :"timestamp without time zone", null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230220202107_create_op_deposits.exs b/apps/explorer/priv/optimism/migrations/20230220202107_create_op_deposits.exs new file mode 100644 index 000000000000..3f313995528c --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230220202107_create_op_deposits.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Migrations.CreateOpDeposits do + use Ecto.Migration + + def change do + create table(:op_deposits, primary_key: false) do + add(:l1_block_number, :bigint, null: false) + add(:l1_block_timestamp, :"timestamp without time zone", null: true) + add(:l1_transaction_hash, :bytea, null: false) + add(:l1_transaction_origin, :bytea, null: false) + add(:l2_transaction_hash, :bytea, null: false, primary_key: true) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:op_deposits, [:l1_block_number])) + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230301105051_rename_fields.exs b/apps/explorer/priv/optimism/migrations/20230301105051_rename_fields.exs new file mode 100644 index 000000000000..d7041587a38f --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230301105051_rename_fields.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.RenameFields do + use Ecto.Migration + + def change do + rename(table(:op_transaction_batches), :l1_tx_hashes, to: :l1_transaction_hashes) + rename(table(:op_transaction_batches), :l1_tx_timestamp, to: :l1_timestamp) + rename(table(:op_output_roots), :l1_tx_hash, to: :l1_transaction_hash) + rename(table(:op_withdrawals), :l2_tx_hash, to: :l2_transaction_hash) + rename(table(:op_withdrawals), :withdrawal_hash, to: :hash) + rename(table(:op_withdrawal_events), :l1_tx_hash, to: :l1_transaction_hash) + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230303125841_add_op_indexes.exs b/apps/explorer/priv/optimism/migrations/20230303125841_add_op_indexes.exs new file mode 100644 index 000000000000..e9db8591c361 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230303125841_add_op_indexes.exs @@ -0,0 +1,8 @@ +defmodule Explorer.Repo.Migrations.AddOpIndexes do + use Ecto.Migration + + def change do + create(index(:op_output_roots, [:l1_block_number])) + create(index(:op_withdrawal_events, [:l1_block_number])) + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230307090655_add_op_frame_sequences_table.exs b/apps/explorer/priv/optimism/migrations/20230307090655_add_op_frame_sequences_table.exs new file mode 100644 index 000000000000..ea7f192af638 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230307090655_add_op_frame_sequences_table.exs @@ -0,0 +1,24 @@ +defmodule Explorer.Repo.Migrations.AddOpFrameSequencesTable do + use Ecto.Migration + + def change do + create table(:op_frame_sequences, primary_key: true) do + add(:l1_transaction_hashes, {:array, :bytea}, null: false) + add(:l1_timestamp, :"timestamp without time zone", null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + + alter table(:op_transaction_batches) do + remove(:l1_transaction_hashes) + remove(:l1_timestamp) + + add( + :frame_sequence_id, + references(:op_frame_sequences, on_delete: :restrict, on_update: :update_all, type: :bigint), + null: false, + after: :epoch_number + ) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20230731130103_modify_collated_gas_price_constraint.exs b/apps/explorer/priv/optimism/migrations/20230731130103_modify_collated_gas_price_constraint.exs new file mode 100644 index 000000000000..59a07d849153 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20230731130103_modify_collated_gas_price_constraint.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Optimism.Migrations.ModifyCollatedGasPriceConstraint do + use Ecto.Migration + + def change do + execute("ALTER TABLE transactions DROP CONSTRAINT collated_gas_price") + + create( + constraint( + :transactions, + :collated_gas_price, + check: "block_hash IS NULL OR gas_price IS NOT NULL OR max_fee_per_gas IS NOT NULL" + ) + ) + end +end diff --git a/apps/explorer/priv/optimism/migrations/20231025102325_add_op_withdrawal_index.exs b/apps/explorer/priv/optimism/migrations/20231025102325_add_op_withdrawal_index.exs new file mode 100644 index 000000000000..3fda19158b86 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20231025102325_add_op_withdrawal_index.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.AddOpWithdrawalIndex do + use Ecto.Migration + + def change do + create(index(:op_withdrawals, :l2_transaction_hash)) + end +end diff --git a/apps/explorer/priv/optimism/migrations/20240124124644_remove_op_epoch_number_field.exs b/apps/explorer/priv/optimism/migrations/20240124124644_remove_op_epoch_number_field.exs new file mode 100644 index 000000000000..652f9551896c --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20240124124644_remove_op_epoch_number_field.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.RemoveOpEpochNumberField do + use Ecto.Migration + + def change do + alter table(:op_transaction_batches) do + remove(:epoch_number) + end + end +end diff --git a/apps/explorer/priv/polygon_zkevm/migrations/20231010093238_add_bridge_tables.exs b/apps/explorer/priv/polygon_zkevm/migrations/20231010093238_add_bridge_tables.exs new file mode 100644 index 000000000000..805acd4d6a70 --- /dev/null +++ b/apps/explorer/priv/polygon_zkevm/migrations/20231010093238_add_bridge_tables.exs @@ -0,0 +1,46 @@ +defmodule Explorer.Repo.PolygonZkevm.Migrations.AddBridgeTables do + use Ecto.Migration + + def change do + create table(:polygon_zkevm_bridge_l1_tokens, primary_key: false) do + add(:id, :identity, primary_key: true, start_value: 0, increment: 1) + add(:address, :bytea, null: false) + add(:decimals, :smallint, null: true, default: nil) + add(:symbol, :string, size: 16, null: true, default: nil) + timestamps(null: false, type: :utc_datetime_usec) + end + + create(unique_index(:polygon_zkevm_bridge_l1_tokens, :address)) + + execute( + "CREATE TYPE polygon_zkevm_bridge_op_type AS ENUM ('deposit', 'withdrawal')", + "DROP TYPE polygon_zkevm_bridge_op_type" + ) + + create table(:polygon_zkevm_bridge, primary_key: false) do + add(:type, :polygon_zkevm_bridge_op_type, null: false, primary_key: true) + add(:index, :integer, null: false, primary_key: true) + add(:l1_transaction_hash, :bytea, null: true) + add(:l2_transaction_hash, :bytea, null: true) + + add( + :l1_token_id, + references(:polygon_zkevm_bridge_l1_tokens, on_delete: :restrict, on_update: :update_all, type: :identity), + null: true + ) + + add(:l1_token_address, :bytea, null: true) + add(:l2_token_address, :bytea, null: true) + add(:amount, :numeric, precision: 100, null: false) + add(:block_number, :bigint, null: true) + add(:block_timestamp, :"timestamp without time zone", null: true) + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:polygon_zkevm_bridge, :l1_token_address)) + + rename(table(:zkevm_lifecycle_l1_transactions), to: table(:polygon_zkevm_lifecycle_l1_transactions)) + rename(table(:zkevm_transaction_batches), to: table(:polygon_zkevm_transaction_batches)) + rename(table(:zkevm_batch_l2_transactions), to: table(:polygon_zkevm_batch_l2_transactions)) + end +end diff --git a/apps/explorer/priv/polygon_zkevm/migrations/20240306080627_make_timestamp_optional.exs b/apps/explorer/priv/polygon_zkevm/migrations/20240306080627_make_timestamp_optional.exs new file mode 100644 index 000000000000..f813ba8ba735 --- /dev/null +++ b/apps/explorer/priv/polygon_zkevm/migrations/20240306080627_make_timestamp_optional.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.PolygonZkevm.Migrations.MakeTimestampOptional do + use Ecto.Migration + + def change do + alter table("polygon_zkevm_transaction_batches") do + modify(:timestamp, :"timestamp without time zone", null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20231212101547_add_block_timestamp_and_consensus_to_transactions.exs b/apps/explorer/priv/repo/migrations/20231212101547_add_block_timestamp_and_consensus_to_transactions.exs new file mode 100644 index 000000000000..458e29604383 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231212101547_add_block_timestamp_and_consensus_to_transactions.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.Migrations.AddBlockTimestampAndConsensusToTransactions do + use Ecto.Migration + + def change do + alter table(:transactions) do + add_if_not_exists(:block_timestamp, :utc_datetime_usec) + add_if_not_exists(:block_consensus, :boolean, default: true) + end + + create_if_not_exists(index(:transactions, :block_timestamp)) + create_if_not_exists(index(:transactions, :block_consensus)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231212102127_create_migrations_status.exs b/apps/explorer/priv/repo/migrations/20231212102127_create_migrations_status.exs new file mode 100644 index 000000000000..0b8bf54a5c3b --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231212102127_create_migrations_status.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.CreateMigrationsStatus do + use Ecto.Migration + + def change do + create table(:migrations_status, primary_key: false) do + add(:migration_name, :string, primary_key: true) + add(:status, :string) + + timestamps() + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20231213085254_add_btree_gin_extension.exs b/apps/explorer/priv/repo/migrations/20231213085254_add_btree_gin_extension.exs new file mode 100644 index 000000000000..b34f9ee059cb --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231213085254_add_btree_gin_extension.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.CreateBtreeGinExtension do + use Ecto.Migration + + def up do + execute("CREATE EXTENSION IF NOT EXISTS btree_gin") + end + + def down do + execute("DROP EXTENSION IF EXISTS btree_gin") + end +end diff --git a/apps/explorer/priv/repo/migrations/20231213090140_add_token_transfers_token_contract_address_token_ids_index.exs b/apps/explorer/priv/repo/migrations/20231213090140_add_token_transfers_token_contract_address_token_ids_index.exs new file mode 100644 index 000000000000..7636fc7b3c2f --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231213090140_add_token_transfers_token_contract_address_token_ids_index.exs @@ -0,0 +1,26 @@ +defmodule Explorer.Repo.Migrations.AddTokenTransfersTokenContractAddressTokenIdsIndex do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def up do + create( + index( + :token_transfers, + [:token_contract_address_hash, :token_ids], + name: "token_transfers_token_contract_address_hash_token_ids_index", + using: "GIN", + concurrently: true + ) + ) + end + + def down do + drop_if_exists( + index(:token_transfers, [:token_contract_address_hash, :token_ids], + name: :token_transfers_token_contract_address_hash_token_ids_index + ), + concurrently: true + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231213101235_drop_token_transfers_token_ids_index.exs b/apps/explorer/priv/repo/migrations/20231213101235_drop_token_transfers_token_ids_index.exs new file mode 100644 index 000000000000..0065d2e26312 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231213101235_drop_token_transfers_token_ids_index.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.DropTokenTransfersTokenIdsIndex do + use Ecto.Migration + + def change do + drop_if_exists(index(:token_transfers, [:token_ids], name: :token_transfers_token_ids_index)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231213152332_alter_log_topic_columns_type.exs b/apps/explorer/priv/repo/migrations/20231213152332_alter_log_topic_columns_type.exs new file mode 100644 index 000000000000..649c95d0dbf1 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231213152332_alter_log_topic_columns_type.exs @@ -0,0 +1,18 @@ +defmodule Explorer.Repo.Migrations.AlterLogTopicColumnsType do + use Ecto.Migration + + def change do + execute(""" + ALTER TABLE logs + ALTER COLUMN first_topic TYPE bytea + USING CAST(REPLACE(first_topic, '0x', '\\x') as bytea), + ALTER COLUMN second_topic TYPE bytea + USING CAST(REPLACE(second_topic, '0x', '\\x') as bytea), + ALTER COLUMN third_topic TYPE bytea + USING CAST(REPLACE(third_topic, '0x', '\\x') as bytea), + ALTER COLUMN fourth_topic TYPE bytea + USING CAST(REPLACE(fourth_topic, '0x', '\\x') as bytea) + ; + """) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231215094615_drop_token_transfers_token_id_column.exs b/apps/explorer/priv/repo/migrations/20231215094615_drop_token_transfers_token_id_column.exs new file mode 100644 index 000000000000..df8637071b31 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231215094615_drop_token_transfers_token_id_column.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.DropTokenTransfersTokenIdColumn do + use Ecto.Migration + + def change do + drop(index(:token_transfers, [:token_id])) + drop(index(:token_transfers, [:token_contract_address_hash, "token_id DESC", "block_number DESC"])) + + alter table(:token_transfers) do + remove(:token_id) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20231215104320_drop_unused_actb_indexes.exs b/apps/explorer/priv/repo/migrations/20231215104320_drop_unused_actb_indexes.exs new file mode 100644 index 000000000000..0f8ad39f3bc0 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231215104320_drop_unused_actb_indexes.exs @@ -0,0 +1,21 @@ +defmodule Explorer.Repo.Migrations.DropUnusedActbIndexes do + use Ecto.Migration + + def change do + drop( + index( + :address_current_token_balances, + [:value], + name: :address_current_token_balances_value, + where: "value IS NOT NULL" + ) + ) + + drop( + index( + :address_current_token_balances, + [:address_hash, :block_number, :token_contract_address_hash] + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231215115638_drop_unused_logs_type_index.exs b/apps/explorer/priv/repo/migrations/20231215115638_drop_unused_logs_type_index.exs new file mode 100644 index 000000000000..fc7df4ddce8c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231215115638_drop_unused_logs_type_index.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.DropUnusedLogsTypeIndex do + use Ecto.Migration + + def change do + drop(index(:logs, [:type], name: :logs_type_index)) + + alter table(:logs) do + remove(:type) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20231215132609_add_index_blocks_refetch_needed.exs b/apps/explorer/priv/repo/migrations/20231215132609_add_index_blocks_refetch_needed.exs new file mode 100644 index 000000000000..aef72e5ed81c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231215132609_add_index_blocks_refetch_needed.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Migrations.AddIndexBlocksRefetchNeeded do + use Ecto.Migration + + def up do + execute(""" + CREATE INDEX consensus_block_hashes_refetch_needed ON blocks(hash) WHERE consensus and refetch_needed; + """) + end + + def down do + execute(""" + DROP INDEX consensus_block_hashes_refetch_needed; + """) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231225113850_transactions_asc_indices.exs b/apps/explorer/priv/repo/migrations/20231225113850_transactions_asc_indices.exs new file mode 100644 index 000000000000..b0f668e89cd5 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231225113850_transactions_asc_indices.exs @@ -0,0 +1,47 @@ +defmodule Explorer.Repo.Migrations.TransactionsAscIndices do + use Ecto.Migration + + def change do + create( + index( + :transactions, + [ + :from_address_hash, + "block_number ASC NULLS LAST", + "index ASC NULLS LAST", + "inserted_at ASC", + "hash DESC" + ], + name: "transactions_from_address_hash_with_pending_index_asc" + ) + ) + + create( + index( + :transactions, + [ + :to_address_hash, + "block_number ASC NULLS LAST", + "index ASC NULLS LAST", + "inserted_at ASC", + "hash DESC" + ], + name: "transactions_to_address_hash_with_pending_index_asc" + ) + ) + + create( + index( + :transactions, + [ + :created_contract_address_hash, + "block_number ASC NULLS LAST", + "index ASC NULLS LAST", + "inserted_at ASC", + "hash DESC" + ], + name: "transactions_created_contract_address_hash_with_pending_index_a" + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231225115026_logs_asc_index.exs b/apps/explorer/priv/repo/migrations/20231225115026_logs_asc_index.exs new file mode 100644 index 000000000000..7e7fc0963d6a --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231225115026_logs_asc_index.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.LogsAscIndex do + use Ecto.Migration + + def change do + create(index(:logs, ["block_number ASC, index ASC"])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231225115100_token_transfers_asc_index.exs b/apps/explorer/priv/repo/migrations/20231225115100_token_transfers_asc_index.exs new file mode 100644 index 000000000000..084ce83adb35 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231225115100_token_transfers_asc_index.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.TokenTransfersAscIndex do + use Ecto.Migration + + def change do + create(index(:token_transfers, ["block_number ASC", "log_index ASC"])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20231227170848_add_proxy_verification_status.exs b/apps/explorer/priv/repo/migrations/20231227170848_add_proxy_verification_status.exs new file mode 100644 index 000000000000..0b20b0a914e2 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231227170848_add_proxy_verification_status.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Migrations.AddProxyVerificationStatus do + use Ecto.Migration + + def change do + create table("proxy_smart_contract_verification_statuses", primary_key: false) do + add(:uid, :string, size: 64, primary_key: true) + add(:status, :int2, null: false) + + add( + :contract_address_hash, + references(:smart_contracts, column: :address_hash, on_delete: :delete_all, type: :bytea) + ) + + timestamps() + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20231229120232_add_smart_contract_audit_reports_table.exs b/apps/explorer/priv/repo/migrations/20231229120232_add_smart_contract_audit_reports_table.exs new file mode 100644 index 000000000000..3b5880acfb8a --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20231229120232_add_smart_contract_audit_reports_table.exs @@ -0,0 +1,39 @@ +defmodule Explorer.Repo.Migrations.AddSmartContractAuditReportsTable do + use Ecto.Migration + + def change do + create table(:smart_contract_audit_reports) do + add(:address_hash, references(:smart_contracts, column: :address_hash, on_delete: :delete_all, type: :bytea), + null: false + ) + + add(:is_approved, :boolean, default: false) + add(:submitter_name, :string, null: false) + add(:submitter_email, :string, null: false) + add(:is_project_owner, :boolean, default: false) + + add(:project_name, :string, null: false) + add(:project_url, :string, null: false) + + add(:audit_company_name, :string, null: false) + add(:audit_report_url, :string, null: false) + add(:audit_publish_date, :date, null: false) + + add(:request_id, :string, null: true) + + add(:comment, :text, null: true) + + timestamps() + end + + create(index(:smart_contract_audit_reports, [:address_hash])) + + create( + unique_index( + :smart_contract_audit_reports, + [:address_hash, :audit_report_url, :audit_publish_date, :audit_company_name], + name: :audit_report_unique_index + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs b/apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs new file mode 100644 index 000000000000..ab43538e4e1e --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.ConstrainNullDateMarketHistory do + use Ecto.Migration + + def change do + alter table(:market_history) do + modify(:date, :date, null: false, from: {:date, null: true}) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20240114181404_enhanced_unfetched_token_balances_index.exs b/apps/explorer/priv/repo/migrations/20240114181404_enhanced_unfetched_token_balances_index.exs new file mode 100644 index 000000000000..dcab0dc92050 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240114181404_enhanced_unfetched_token_balances_index.exs @@ -0,0 +1,20 @@ +defmodule Explorer.Repo.Migrations.EnhancedUnfetchedTokenBalancesIndex do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def up do + execute(""" + CREATE INDEX CONCURRENTLY unfetched_address_token_balances_index on address_token_balances(id) + WHERE ( + ((address_hash != '\\x0000000000000000000000000000000000000000' AND token_type = 'ERC-721') OR token_type = 'ERC-20' OR token_type = 'ERC-1155') AND (value_fetched_at IS NULL OR value IS NULL) + ); + """) + end + + def down do + execute(""" + DROP INDEX unfetched_address_token_balances_index; + """) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240122102141_add_token_type_to_token_transfers.exs b/apps/explorer/priv/repo/migrations/20240122102141_add_token_type_to_token_transfers.exs new file mode 100644 index 000000000000..7ba7ddff029b --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240122102141_add_token_type_to_token_transfers.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.Migrations.AddTokenTypeToTokenTransfers do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + alter table(:token_transfers) do + add_if_not_exists(:token_type, :string) + end + + create_if_not_exists(index(:token_transfers, :token_type, concurrently: true)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240123102336_add_tokens_cataloged_index.exs b/apps/explorer/priv/repo/migrations/20240123102336_add_tokens_cataloged_index.exs new file mode 100644 index 000000000000..b0157fb6a9c5 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240123102336_add_tokens_cataloged_index.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Migrations.AddTokensCatalogedIndex do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + create( + index( + :tokens, + ~w(cataloged)a, + name: :uncataloged_tokens, + where: ~s|"cataloged" = false|, + concurrently: true + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240129112623_add_smart_contract_license_type.exs b/apps/explorer/priv/repo/migrations/20240129112623_add_smart_contract_license_type.exs new file mode 100644 index 000000000000..91bea9e1b861 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240129112623_add_smart_contract_license_type.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.AddSmartContractLicenseType do + use Ecto.Migration + + def change do + alter table("smart_contracts") do + add(:license_type, :int2, null: false, default: 1) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20240219143204_add_volume_24h_to_tokens.exs b/apps/explorer/priv/repo/migrations/20240219143204_add_volume_24h_to_tokens.exs new file mode 100644 index 000000000000..cf3c9ad7b3b8 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240219143204_add_volume_24h_to_tokens.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.AddVolume24hToTokens do + use Ecto.Migration + + def change do + alter table(:tokens) do + add(:volume_24h, :decimal) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20240219152810_add_block_consensus_to_token_transfers.exs b/apps/explorer/priv/repo/migrations/20240219152810_add_block_consensus_to_token_transfers.exs new file mode 100644 index 000000000000..253c952ba4ea --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240219152810_add_block_consensus_to_token_transfers.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.Migrations.AddBlockConsensusToTokenTransfers do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + alter table(:token_transfers) do + add_if_not_exists(:block_consensus, :boolean, default: true) + end + + create_if_not_exists(index(:token_transfers, :block_consensus, concurrently: true)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240224112210_create_index_pending_block_operations_block_number.exs b/apps/explorer/priv/repo/migrations/20240224112210_create_index_pending_block_operations_block_number.exs new file mode 100644 index 000000000000..ad590ae77aa6 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240224112210_create_index_pending_block_operations_block_number.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.CreateIndexPendingBlockOperationsBlockNumber do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + create_if_not_exists(index(:pending_block_operations, :block_number, concurrently: true)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240226074456_create_massive_blocks.exs b/apps/explorer/priv/repo/migrations/20240226074456_create_massive_blocks.exs new file mode 100644 index 000000000000..824a4f8f271c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240226074456_create_massive_blocks.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.CreateMassiveBlocks do + use Ecto.Migration + + def change do + create table(:massive_blocks, primary_key: false) do + add(:number, :bigint, primary_key: true) + + timestamps() + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20240226151331_add_secondary_coin_market_history.exs b/apps/explorer/priv/repo/migrations/20240226151331_add_secondary_coin_market_history.exs new file mode 100644 index 000000000000..799cf62ab01c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240226151331_add_secondary_coin_market_history.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.AddSecondaryCoinMarketHistory do + use Ecto.Migration + + def change do + alter table(:market_history) do + add(:secondary_coin, :boolean, default: false) + end + + drop_if_exists(unique_index(:market_history, [:date])) + create(unique_index(:market_history, [:date, :secondary_coin])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240227115149_add_smart_contracts_name_text_index.exs b/apps/explorer/priv/repo/migrations/20240227115149_add_smart_contracts_name_text_index.exs new file mode 100644 index 000000000000..8cf53911574d --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240227115149_add_smart_contracts_name_text_index.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.Migrations.AddSmartContractsNameTextIndex do + use Ecto.Migration + + def up do + execute(""" + CREATE INDEX IF NOT EXISTS smart_contracts_trgm_idx ON smart_contracts USING GIN (to_tsvector('english', name)) + """) + end + + def down do + execute("DROP INDEX IF EXISTS smart_contracts_trgm_idx") + end +end diff --git a/apps/explorer/priv/repo/migrations/20240308123508_token_transfers_add_from_address_hash_block_number_index.exs b/apps/explorer/priv/repo/migrations/20240308123508_token_transfers_add_from_address_hash_block_number_index.exs new file mode 100644 index 000000000000..5d7d27f14474 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240308123508_token_transfers_add_from_address_hash_block_number_index.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.TokenTransfersAddFromAddressHashBlockNumberIndex do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + create_if_not_exists(index(:token_transfers, [:from_address_hash, :block_number], concurrently: true)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20240313195728_token_transfers_add_to_address_hash_block_number_index.exs b/apps/explorer/priv/repo/migrations/20240313195728_token_transfers_add_to_address_hash_block_number_index.exs new file mode 100644 index 000000000000..cef07e10e777 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240313195728_token_transfers_add_to_address_hash_block_number_index.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.TokenTransfersAddToAddressHashBlockNumberIndex do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + create_if_not_exists(index(:token_transfers, [:to_address_hash, :block_number], concurrently: true)) + end +end diff --git a/apps/explorer/priv/rsk/migrations/20230724094744_add_rootstock_fields_to_blocks.exs b/apps/explorer/priv/rsk/migrations/20230724094744_add_rootstock_fields_to_blocks.exs new file mode 100644 index 000000000000..40bbbc79793b --- /dev/null +++ b/apps/explorer/priv/rsk/migrations/20230724094744_add_rootstock_fields_to_blocks.exs @@ -0,0 +1,13 @@ +defmodule Explorer.Repo.RSK.Migrations.AddRootstockFieldsToBlocks do + use Ecto.Migration + + def change do + alter table(:blocks) do + add(:minimum_gas_price, :decimal) + add(:bitcoin_merged_mining_header, :bytea) + add(:bitcoin_merged_mining_coinbase_transaction, :bytea) + add(:bitcoin_merged_mining_merkle_proof, :bytea) + add(:hash_for_merged_mining, :bytea) + end + end +end diff --git a/apps/explorer/priv/shibarium/migrations/20231024091228_add_bridge_table.exs b/apps/explorer/priv/shibarium/migrations/20231024091228_add_bridge_table.exs new file mode 100644 index 000000000000..4fb2fe71c99a --- /dev/null +++ b/apps/explorer/priv/shibarium/migrations/20231024091228_add_bridge_table.exs @@ -0,0 +1,34 @@ +defmodule Explorer.Repo.Shibarium.Migrations.AddBridgeTable do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE shibarium_bridge_operation_type AS ENUM ('deposit', 'withdrawal')", + "DROP TYPE shibarium_bridge_operation_type" + ) + + execute( + "CREATE TYPE shibarium_bridge_token_type AS ENUM ('bone', 'eth', 'other')", + "DROP TYPE shibarium_bridge_token_type" + ) + + create table(:shibarium_bridge, primary_key: false) do + add(:user, :bytea, null: false) + add(:amount_or_id, :numeric, precision: 100, null: true) + add(:erc1155_ids, {:array, :numeric}, precision: 78, scale: 0, null: true) + add(:erc1155_amounts, {:array, :decimal}, null: true) + add(:operation_hash, :bytea, primary_key: true) + add(:operation_type, :shibarium_bridge_operation_type, null: false) + add(:l1_transaction_hash, :bytea, primary_key: true) + add(:l1_block_number, :bigint, null: true) + add(:l2_transaction_hash, :bytea, primary_key: true) + add(:l2_block_number, :bigint, null: true) + add(:token_type, :shibarium_bridge_token_type, null: false) + add(:timestamp, :"timestamp without time zone", null: true) + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:shibarium_bridge, [:l1_block_number, :operation_type])) + create(index(:shibarium_bridge, [:l2_block_number, :operation_type])) + end +end diff --git a/apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs b/apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs new file mode 100644 index 000000000000..67e5580fef13 --- /dev/null +++ b/apps/explorer/priv/stability/migrations/20240203091010_add_stability_validators.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Stability.Migrations.AddStabilityValidators do + use Ecto.Migration + + def change do + create table(:validators_stability, primary_key: false) do + add(:address_hash, :bytea, null: false, primary_key: true) + add(:state, :integer, default: 0) + + timestamps() + end + + create_if_not_exists(index(:validators_stability, ["state ASC", "address_hash ASC"])) + end +end diff --git a/apps/explorer/priv/zk_sync/migrations/20211202082101_make_tranaction_r_s_v_optional.exs b/apps/explorer/priv/zk_sync/migrations/20211202082101_make_tranaction_r_s_v_optional.exs new file mode 100644 index 000000000000..7bf465fb5bda --- /dev/null +++ b/apps/explorer/priv/zk_sync/migrations/20211202082101_make_tranaction_r_s_v_optional.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.ZkSync.Migrations.MakeTransactionRSVOptional do + use Ecto.Migration + + def change do + alter table(:transactions) do + modify(:r, :numeric, precision: 100, null: true) + end + + alter table(:transactions) do + modify(:s, :numeric, precision: 100, null: true) + end + + alter table(:transactions) do + modify(:v, :numeric, precision: 100, null: true) + end + end +end diff --git a/apps/explorer/priv/zk_sync/migrations/20231213171043_create_zksync_tables.exs b/apps/explorer/priv/zk_sync/migrations/20231213171043_create_zksync_tables.exs new file mode 100644 index 000000000000..1e7d02c1d7c0 --- /dev/null +++ b/apps/explorer/priv/zk_sync/migrations/20231213171043_create_zksync_tables.exs @@ -0,0 +1,82 @@ +defmodule Explorer.Repo.ZkSync.Migrations.CreateZkSyncTables do + use Ecto.Migration + + def change do + create table(:zksync_lifecycle_l1_transactions, primary_key: false) do + add(:id, :integer, null: false, primary_key: true) + add(:hash, :bytea, null: false) + add(:timestamp, :"timestamp without time zone", null: false) + timestamps(null: false, type: :utc_datetime_usec) + end + + create(unique_index(:zksync_lifecycle_l1_transactions, :hash)) + + create table(:zksync_transaction_batches, primary_key: false) do + add(:number, :integer, null: false, primary_key: true) + add(:timestamp, :"timestamp without time zone", null: false) + add(:l1_tx_count, :integer, null: false) + add(:l2_tx_count, :integer, null: false) + add(:root_hash, :bytea, null: false) + add(:l1_gas_price, :numeric, precision: 100, null: false) + add(:l2_fair_gas_price, :numeric, precision: 100, null: false) + add(:start_block, :integer, null: false) + add(:end_block, :integer, null: false) + + add( + :commit_id, + references(:zksync_lifecycle_l1_transactions, on_delete: :restrict, on_update: :update_all, type: :integer), + null: true + ) + + add( + :prove_id, + references(:zksync_lifecycle_l1_transactions, on_delete: :restrict, on_update: :update_all, type: :integer), + null: true + ) + + add( + :execute_id, + references(:zksync_lifecycle_l1_transactions, on_delete: :restrict, on_update: :update_all, type: :integer), + null: true + ) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create table(:zksync_batch_l2_transactions, primary_key: false) do + add( + :batch_number, + references(:zksync_transaction_batches, + column: :number, + on_delete: :delete_all, + on_update: :update_all, + type: :integer + ), + null: false + ) + + add(:hash, :bytea, null: false, primary_key: true) + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:zksync_batch_l2_transactions, :batch_number)) + + create table(:zksync_batch_l2_blocks, primary_key: false) do + add( + :batch_number, + references(:zksync_transaction_batches, + column: :number, + on_delete: :delete_all, + on_update: :update_all, + type: :integer + ), + null: false + ) + + add(:hash, :bytea, null: false, primary_key: true) + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:zksync_batch_l2_blocks, :batch_number)) + end +end diff --git a/apps/explorer/test/explorer/account/notifier/notify_test.exs b/apps/explorer/test/explorer/account/notifier/notify_test.exs index bc7480cc3ec2..25a7fd34a3ee 100644 --- a/apps/explorer/test/explorer/account/notifier/notify_test.exs +++ b/apps/explorer/test/explorer/account/notifier/notify_test.exs @@ -68,7 +68,7 @@ defmodule Explorer.Account.Notifier.NotifyTest do hash: _tx_hash } = with_block(insert(:transaction, to_address: %Chain.Address{hash: address_hash})) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) amount = Wei.to(tx.value, :ether) notify = Notify.call([tx]) @@ -86,5 +86,61 @@ defmodule Explorer.Account.Notifier.NotifyTest do assert wn.tx_fee == fee assert wn.type == "COIN" end + + test "ignore new notification when limit is reached" do + old_envs = Application.get_env(:explorer, Explorer.Account) + + Application.put_env(:explorer, Explorer.Account, Keyword.put(old_envs, :notifications_limit_for_30_days, 1)) + + wa = + %WatchlistAddress{address_hash: address_hash} = + build(:account_watchlist_address, watch_coin_input: true) + |> Repo.account_repo().insert!() + + _watchlist_address = Repo.preload(wa, watchlist: :identity) + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction, to_address: %Chain.Address{hash: address_hash})) + + {_, fee} = Transaction.fee(tx, :gwei) + amount = Wei.to(tx.value, :ether) + notify = Notify.call([tx]) + + wn = + WatchlistNotification + |> first + |> Repo.account_repo().one() + + assert notify == [[:ok]] + + assert wn.amount == amount + assert wn.direction == "incoming" + assert wn.method == "transfer" + assert wn.subject == "Coin transaction" + assert wn.tx_fee == fee + assert wn.type == "COIN" + address = Repo.get(Chain.Address, address_hash) + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction, to_address: address)) + + Notify.call([tx]) + + WatchlistNotification + |> first + |> Repo.account_repo().one!() + + Application.put_env(:explorer, Explorer.Account, old_envs) + end end end diff --git a/apps/explorer/test/explorer/account/notifier/summary_test.exs b/apps/explorer/test/explorer/account/notifier/summary_test.exs index 2f899d8e6ff8..37d18f95dc34 100644 --- a/apps/explorer/test/explorer/account/notifier/summary_test.exs +++ b/apps/explorer/test/explorer/account/notifier/summary_test.exs @@ -4,7 +4,6 @@ defmodule Explorer.Account.Notifier.SummaryTest do import Explorer.Factory alias Explorer.Account.Notifier.Summary - alias Explorer.Chain alias Explorer.Chain.{TokenTransfer, Transaction, Wei} alias Explorer.Repo @@ -18,7 +17,7 @@ defmodule Explorer.Account.Notifier.SummaryTest do hash: tx_hash } = with_block(insert(:transaction)) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) amount = Wei.to(tx.value, :ether) assert Summary.process(tx) == [ @@ -65,7 +64,7 @@ defmodule Explorer.Account.Notifier.SummaryTest do |> with_contract_creation(contract_address) |> with_block(block) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) amount = Wei.to(tx.value, :ether) assert Summary.process(tx) == [ @@ -107,7 +106,7 @@ defmodule Explorer.Account.Notifier.SummaryTest do :token ]) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) token_decimals = Decimal.to_integer(token.decimals) @@ -159,7 +158,7 @@ defmodule Explorer.Account.Notifier.SummaryTest do :token ]) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) assert Summary.process(transfer) == [ %Summary{ @@ -205,7 +204,7 @@ defmodule Explorer.Account.Notifier.SummaryTest do :token ]) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) assert Summary.process(transfer) == [ %Summary{ @@ -251,7 +250,7 @@ defmodule Explorer.Account.Notifier.SummaryTest do :token ]) - {_, fee} = Chain.fee(tx, :gwei) + {_, fee} = Transaction.fee(tx, :gwei) assert Summary.process(transfer) == [ %Summary{ @@ -268,5 +267,112 @@ defmodule Explorer.Account.Notifier.SummaryTest do } ] end + + test "ERC-404 Token transfer with token id" do + token = insert(:token, type: "ERC-404") + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction)) + + transfer = + %TokenTransfer{ + amount: _amount, + block_number: block_number, + from_address: from_address, + to_address: to_address + } = + :token_transfer + |> insert( + transaction: tx, + token_ids: [42], + token_contract_address: token.contract_address + ) + |> Repo.preload([ + :token + ]) + + {_, fee} = Transaction.fee(tx, :gwei) + + token_decimals = Decimal.to_integer(token.decimals) + + decimals = Decimal.new(Integer.pow(10, token_decimals)) + + amount = Decimal.div(transfer.amount, decimals) + + assert Summary.process(transfer) == [ + %Summary{ + amount: amount, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "Infinite Token", + subject: "42", + to_address_hash: to_address.hash, + transaction_hash: tx.hash, + tx_fee: fee, + type: "ERC-404" + } + ] + end + + test "ERC-404 Token transfer without token id" do + token = insert(:token, type: "ERC-404") + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction)) + + transfer = + %TokenTransfer{ + amount: _amount, + block_number: block_number, + from_address: from_address, + to_address: to_address + } = + :token_transfer + |> insert( + transaction: tx, + token_ids: [], + token_contract_address: token.contract_address + ) + |> Repo.preload([ + :token + ]) + + {_, fee} = Transaction.fee(tx, :gwei) + + token_decimals = Decimal.to_integer(token.decimals) + + decimals = Decimal.new(Integer.pow(10, token_decimals)) + + amount = Decimal.div(transfer.amount, decimals) + + IO.inspect("Gimme") + IO.inspect(Summary.process(transfer)) + + assert Summary.process(transfer) == [ + %Summary{ + amount: amount, + block_number: block_number, + from_address_hash: from_address.hash, + method: "transfer", + name: "Infinite Token", + subject: "ERC-404", + to_address_hash: to_address.hash, + transaction_hash: tx.hash, + tx_fee: fee, + type: "ERC-404" + } + ] + end end end diff --git a/apps/explorer/test/explorer/bloom_filter_test.exs b/apps/explorer/test/explorer/bloom_filter_test.exs new file mode 100644 index 000000000000..e9ec4a2c5106 --- /dev/null +++ b/apps/explorer/test/explorer/bloom_filter_test.exs @@ -0,0 +1,63 @@ +defmodule Explorer.BloomFilterTest do + use Explorer.DataCase + + alias Explorer.BloomFilter + + describe "Test bloom filter for random Goerli transactions" do + # {"jsonrpc":"2.0","id": 0,"method":"eth_getTransactionReceipt","params":["0xFE524295C6C01AB25645035A228387BF0E64C8AF429F3DD9D6EF2E3B05337839"]} + test "#1 (0xFE524295C6C01AB25645035A228387BF0E64C8AF429F3DD9D6EF2E3B05337839)" do + log_1 = + insert(:log, + first_topic: "0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32", + second_topic: "0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d", + third_topic: "0x000000000000000000000000e38ecdf3cfbaf5cf347e6a3d6490eb34e3a0119d", + fourth_topic: "0x0000000000000000000000000000000000000000000000000000000000000000", + address_hash: "0xe93c8cd0d409341205a592f8c4ac1a5fe5585cfa", + address: nil + ) + + assert BloomFilter.logs_bloom([log_1]) |> Base.encode16(case: :lower) == + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000040000000000000000000000000002000000000000000000000000000000000000000000000000030000000000000000000800000000000000000000000000000000000000000000000002000000008000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000002000000000000000080000000000000000000000" + end + + test "#2 (0x9f44d7080354147fc42ee0eb62c8f77d0477e7686d18debcb81f90b0d54ea1d1)" do + log_1 = + insert(:log, + first_topic: "0x9866f8ddfe70bb512b2f2b28b49d4017c43f7ba775f1a20c61c13eea8cdac111", + second_topic: nil, + third_topic: nil, + fourth_topic: nil, + address_hash: "0xd5c325d183c592c94998000c5e0eed9e6655c020", + address: nil + ) + + log_2 = + insert(:log, + first_topic: "0xd342ddf7a308dec111745b00315c14b7efb2bdae570a6856e088ed0c65a3576c", + second_topic: nil, + third_topic: nil, + fourth_topic: nil, + address_hash: "0xd5c325d183c592c94998000c5e0eed9e6655c020", + address: nil + ) + + assert BloomFilter.logs_bloom([log_1, log_2]) |> Base.encode16(case: :lower) == + "00000000010002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000800000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000020000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000" + end + + test "#3 (0x2548b6514211bafdfeff37dc184c4700c8ca7056ac2a119bef5a98f8a79662cc)" do + log_1 = + insert(:log, + first_topic: "0x1a37b94876a9c4d5697c33a6fc124022beba6ce60e84469f41d49536d2a55924", + second_topic: "0x000000000000000000000000000000000000000000000000000000000001ba63", + third_topic: "0x00000000000000000000000000000000000000000000000000f8b0a10e470000", + fourth_topic: "0x0000000000000000000000000000000000000000000000000000000000000002", + address_hash: "0x2a5ccc8813d89119263b49f567c541e925c75f13", + address: nil + ) + + assert BloomFilter.logs_bloom([log_1]) |> Base.encode16(case: :lower) == + "04000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000420000000000000000000000800000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000020000000000000000000000000000000100000000000100000000000002010000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000010000000000000" + end + end +end diff --git a/apps/explorer/test/explorer/chain/address/token_balance_test.exs b/apps/explorer/test/explorer/chain/address/token_balance_test.exs index 9a72837f6a5f..3e1a8018289a 100644 --- a/apps/explorer/test/explorer/chain/address/token_balance_test.exs +++ b/apps/explorer/test/explorer/chain/address/token_balance_test.exs @@ -46,6 +46,7 @@ defmodule Explorer.Chain.Address.TokenBalanceTest do :token_balance, address: burn_address, token_contract_address_hash: token.contract_address_hash, + token_type: "ERC-721", value_fetched_at: nil ) diff --git a/apps/explorer/test/explorer/chain/address_test.exs b/apps/explorer/test/explorer/chain/address_test.exs index 213913c766a8..c6ca32494652 100644 --- a/apps/explorer/test/explorer/chain/address_test.exs +++ b/apps/explorer/test/explorer/chain/address_test.exs @@ -6,6 +6,8 @@ defmodule Explorer.Chain.AddressTest do alias Explorer.Chain.Address alias Explorer.Repo + setup :verify_on_exit! + describe "changeset/2" do test "with valid attributes" do params = params_for(:address) @@ -61,4 +63,74 @@ defmodule Explorer.Chain.AddressTest do assert str("0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb") == "0xD1220A0Cf47c7B9BE7a2e6ba89F429762E7B9adB" end end + + describe "list_top_addresses/0" do + test "without addresses with balance > 0" do + insert(:address, fetched_coin_balance: 0) + assert [] = Address.list_top_addresses() + end + + test "with top addresses in order" do + address_hashes = + 4..1 + |> Enum.map(&insert(:address, fetched_coin_balance: &1)) + |> Enum.map(& &1.hash) + + assert address_hashes == + Address.list_top_addresses() + |> Enum.map(fn {address, _transaction_count} -> address end) + |> Enum.map(& &1.hash) + end + + # flaky test + # test "with top addresses in order with matching value" do + # test_hashes = + # 4..0 + # |> Enum.map(&Explorer.Chain.Hash.cast(Explorer.Chain.Hash.Address, &1)) + # |> Enum.map(&elem(&1, 1)) + + # tail = + # 4..1 + # |> Enum.map(&insert(:address, fetched_coin_balance: &1, hash: Enum.fetch!(test_hashes, &1 - 1))) + # |> Enum.map(& &1.hash) + + # first_result_hash = + # :address + # |> insert(fetched_coin_balance: 4, hash: Enum.fetch!(test_hashes, 4)) + # |> Map.fetch!(:hash) + + # assert [first_result_hash | tail] == + # Address.list_top_addresses() + # |> Enum.map(fn {address, _transaction_count} -> address end) + # |> Enum.map(& &1.hash) + # end + + # flaky test + # test "paginates addresses" do + # test_hashes = + # 4..0 + # |> Enum.map(&Explorer.Chain.Hash.cast(Explorer.Chain.Hash.Address, &1)) + # |> Enum.map(&elem(&1, 1)) + + # result = + # 4..1 + # |> Enum.map(&insert(:address, fetched_coin_balance: &1, hash: Enum.fetch!(test_hashes, &1 - 1))) + # |> Enum.map(& &1.hash) + + # options = [paging_options: %PagingOptions{page_size: 1}] + + # [{top_address, _}] = Chain.list_top_addresses(options) + # assert top_address.hash == List.first(result) + + # tail_options = [ + # paging_options: %PagingOptions{key: {top_address.fetched_coin_balance.value, top_address.hash}, page_size: 3} + # ] + + # tail_result = tail_options |> Address.list_top_addresses() |> Enum.map(fn {address, _} -> address.hash end) + + # [_ | expected_tail] = result + + # assert tail_result == expected_tail + # end + end end diff --git a/apps/explorer/test/explorer/chain/beacon/reader_test.exs b/apps/explorer/test/explorer/chain/beacon/reader_test.exs new file mode 100644 index 000000000000..d6633364c231 --- /dev/null +++ b/apps/explorer/test/explorer/chain/beacon/reader_test.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Chain.Beacon.ReaderTest do + use Explorer.DataCase + + alias Explorer.Chain.Beacon.Reader + + if Application.compile_env(:explorer, :chain_type) == "ethereum" do + doctest Reader + end +end diff --git a/apps/explorer/test/explorer/chain/block_test.exs b/apps/explorer/test/explorer/chain/block_test.exs index 35f14c54795c..3d09e2c592f6 100644 --- a/apps/explorer/test/explorer/chain/block_test.exs +++ b/apps/explorer/test/explorer/chain/block_test.exs @@ -2,7 +2,7 @@ defmodule Explorer.Chain.BlockTest do use Explorer.DataCase alias Ecto.Changeset - alias Explorer.Chain.Block + alias Explorer.Chain.{Block, Wei} describe "changeset/2" do test "with valid attributes" do @@ -58,4 +58,86 @@ defmodule Explorer.Chain.BlockTest do assert Enum.member?(results, unrewarded_block.hash) end end + + describe "block_reward_by_parts/1" do + setup do + {:ok, emission_reward: insert(:emission_reward)} + end + + test "without uncles", %{emission_reward: %{reward: reward, block_range: range}} do + block = build(:block, number: range.from, base_fee_per_gas: 5, uncles: []) + + tx1 = build(:transaction, gas_price: 1, gas_used: 1, block_number: block.number, block_hash: block.hash) + tx2 = build(:transaction, gas_price: 1, gas_used: 2, block_number: block.number, block_hash: block.hash) + + tx3 = + build(:transaction, + gas_price: 1, + gas_used: 3, + block_number: block.number, + block_hash: block.hash, + max_priority_fee_per_gas: 1 + ) + + expected_transaction_fees = %Wei{value: Decimal.new(6)} + expected_burnt_fees = %Wei{value: Decimal.new(30)} + expected_uncle_reward = %Wei{value: Decimal.new(0)} + + assert %{ + static_reward: ^reward, + transaction_fees: ^expected_transaction_fees, + burnt_fees: ^expected_burnt_fees, + uncle_reward: ^expected_uncle_reward + } = Block.block_reward_by_parts(block, [tx1, tx2, tx3]) + end + + test "with uncles", %{emission_reward: %{reward: reward, block_range: range}} do + block = + build(:block, number: range.from, uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]) + + expected_uncle_reward = Wei.div(reward, 32) + + assert %{uncle_reward: ^expected_uncle_reward} = Block.block_reward_by_parts(block, []) + end + end + + describe "next_block_base_fee_per_gas" do + test "with no blocks in the database returns nil" do + assert Block.next_block_base_fee_per_gas() == nil + end + + test "ignores non consensus blocks" do + insert(:block, consensus: false, base_fee_per_gas: Wei.from(Decimal.new(1), :wei)) + assert Block.next_block_base_fee_per_gas() == nil + end + + test "returns the next block base fee" do + insert(:block, + consensus: true, + base_fee_per_gas: Wei.from(Decimal.new(1000), :wei), + gas_limit: Decimal.new(30_000_000), + gas_used: Decimal.new(15_000_000) + ) + + assert Block.next_block_base_fee_per_gas() == Decimal.new(1000) + + insert(:block, + consensus: true, + base_fee_per_gas: Wei.from(Decimal.new(1000), :wei), + gas_limit: Decimal.new(30_000_000), + gas_used: Decimal.new(3_000_000) + ) + + assert Block.next_block_base_fee_per_gas() == Decimal.new(900) + + insert(:block, + consensus: true, + base_fee_per_gas: Wei.from(Decimal.new(1000), :wei), + gas_limit: Decimal.new(30_000_000), + gas_used: Decimal.new(27_000_000) + ) + + assert Block.next_block_base_fee_per_gas() == Decimal.new(1100) + end + end end diff --git a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs index d7004d11d0ed..5fa0fc97a454 100644 --- a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs +++ b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs @@ -1,79 +1,34 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do use Explorer.DataCase - import Mox - alias Explorer.Chain.Cache.GasPriceOracle - - @block %{ - "difficulty" => "0x0", - "gasLimit" => "0x0", - "gasUsed" => "0x0", - "hash" => "0x29c850324e357f3c0c836d79860c5af55f7b651e5d7ee253c1af1b14908af49c", - "extraData" => "0x0", - "logsBloom" => "0x0", - "miner" => "0x0", - "number" => "0x1", - "parentHash" => "0x0", - "receiptsRoot" => "0x0", - "size" => "0x0", - "sha3Uncles" => "0x0", - "stateRoot" => "0x0", - "timestamp" => "0x0", - "baseFeePerGas" => "0x1DCD6500", - "totalDifficulty" => "0x0", - "transactions" => [ - %{ - "blockHash" => "0x29c850324e357f3c0c836d79860c5af55f7b651e5d7ee253c1af1b14908af49c", - "blockNumber" => "0x1", - "from" => "0x0", - "gas" => "0x0", - "gasPrice" => "0x0", - "hash" => "0xa2e81bb56b55ba3dab2daf76501b50dfaad240cccb905dbf89d65c7a84a4a48e", - "input" => "0x", - "nonce" => "0x0", - "r" => "0x0", - "s" => "0x0", - "to" => "0x0", - "transactionIndex" => "0x0", - "v" => "0x0", - "value" => "0x0" - } - ], - "transactionsRoot" => "0x0", - "uncles" => [] - } + alias Explorer.Chain.Wei + alias Explorer.Counters.AverageBlockTime describe "get_average_gas_price/4" do test "returns nil percentile values if no blocks in the DB" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns nil percentile values if blocks are empty in the DB" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - insert(:block) insert(:block) insert(:block) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end - test "returns nil percentile values for blocks with failed txs in the DB" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - + test "returns gas prices for blocks with failed txs in the DB" do block = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") :transaction @@ -89,17 +44,17 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" ) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{ + :ok, + %{ + average: %{price: 0.01}, + fast: %{price: 0.01}, + slow: %{price: 0.01} + } + }, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns nil percentile values for transactions with 0 gas price aka 'whitelisted transactions' in the DB" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729") @@ -127,17 +82,15 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" ) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) end test "returns the same percentile values if gas price is the same over transactions" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729") @@ -165,17 +118,15 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" ) - assert {:ok, - %{ - "slow" => 1.0, - "average" => 1.0, - "fast" => 1.0 - }} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) + assert {{:ok, + %{ + slow: %{price: 1.0}, + average: %{price: 1.0}, + fast: %{price: 1.0} + }}, _} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) end test "returns correct min gas price from the block" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729") @@ -215,17 +166,15 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" ) - assert {:ok, - %{ - "slow" => 1.0, - "average" => 2.0, - "fast" => 2.0 - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: %{price: 1.0}, + average: %{price: 2.0}, + fast: %{price: 2.0} + }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns correct average percentile" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729") block3 = insert(:block, number: 102, hash: "0x659b2a1cc4dd1a5729900cf0c81c471d1c7891b2517bf9466f7fba56ead2fca0") @@ -266,17 +215,197 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x7d4bc5569053fc29f471901e967c9e60205ac7a122b0e9a789683652c34cc11a" ) - assert {:ok, - %{ - "average" => 3.34 - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + average: %{price: 3.34} + }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns correct gas price for EIP-1559 transactions" do - expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - block1 = insert(:block, number: 100, hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391") - block2 = insert(:block, number: 101, hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729") + + block2 = + insert(:block, + number: 101, + hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729", + gas_limit: Decimal.new(10_000_000), + gas_used: Decimal.new(5_000_000), + base_fee_per_gas: Wei.from(Decimal.new(500_000_000), :wei) + ) + + :transaction + |> insert( + status: :ok, + block_hash: block1.hash, + block_number: block1.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 1, + gas_price: 3_000_000_000, + max_priority_fee_per_gas: 3_000_000_000, + max_fee_per_gas: 3_000_000_000, + hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" + ) + + assert {{:ok, + %{ + # including base fee + slow: %{price: 1.25}, + average: %{price: 2.25} + }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + end + + test "return gas prices with time if available" do + block1 = + insert(:block, + number: 100, + hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391", + timestamp: ~U[2023-12-12 12:12:30.000000Z] + ) + + block2 = + insert(:block, + number: 101, + hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729", + timestamp: ~U[2023-12-12 12:13:00.000000Z], + gas_limit: Decimal.new(10_000_000), + gas_used: Decimal.new(5_000_000), + base_fee_per_gas: Wei.from(Decimal.new(500_000_000), :wei) + ) + + :transaction + |> insert( + status: :ok, + block_hash: block1.hash, + block_number: block1.number, + block_timestamp: block1.timestamp, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269", + earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z] + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + block_timestamp: block2.timestamp, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03", + earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z] + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + block_timestamp: block2.timestamp, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 1, + gas_price: 3_000_000_000, + max_priority_fee_per_gas: 3_000_000_000, + max_fee_per_gas: 3_000_000_000, + hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016", + earliest_processing_start: ~U[2023-12-12 12:12:55.000000Z] + ) + + assert {{ + :ok, + %{ + average: %{price: 2.25, time: 15000.0}, + fast: %{price: 2.25, time: 15000.0}, + slow: %{price: 1.25, time: 17500.0} + } + }, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + end + + test "return gas prices with average block time if earliest_processing_start is not available" do + average_block_time_old_env = Application.get_env(:explorer, AverageBlockTime) + gas_price_oracle_old_env = Application.get_env(:explorer, GasPriceOracle) + + Application.put_env(:explorer, GasPriceOracle, + safelow_time_coefficient: 2.5, + average_time_coefficient: 1.5, + fast_time_coefficient: 1 + ) + + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + start_supervised!(AverageBlockTime) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, average_block_time_old_env) + Application.put_env(:explorer, GasPriceOracle, gas_price_oracle_old_env) + end) + + block_number = 99_999_999 + first_timestamp = ~U[2023-12-12 12:12:30.000000Z] + + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + 1 + i, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 12) + ) + end) + + block1 = + insert(:block, + number: block_number + 102, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -10) + ) + + block2 = + insert(:block, + number: block_number + 103, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -7), + gas_limit: Decimal.new(10_000_000), + gas_used: Decimal.new(5_000_000), + base_fee_per_gas: Wei.from(Decimal.new(500_000_000), :wei) + ) + + AverageBlockTime.refresh() :transaction |> insert( @@ -320,13 +449,16 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" ) - assert {:ok, - %{ - # including base fee - "slow" => 1.5, - "average" => 2.5, - "fast" => 2.5 - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + AverageBlockTime.refresh() + + assert {{ + :ok, + %{ + average: %{price: 2.25, time: 1500.0}, + fast: %{price: 2.25, time: 1000.0}, + slow: %{price: 1.25, time: 2500.0} + } + }, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end end end diff --git a/apps/explorer/test/explorer/chain/csv_export/address_log_csv_exporter_test.exs b/apps/explorer/test/explorer/chain/csv_export/address_log_csv_exporter_test.exs index bff1ca818d33..e70b2eb9cb25 100644 --- a/apps/explorer/test/explorer/chain/csv_export/address_log_csv_exporter_test.exs +++ b/apps/explorer/test/explorer/chain/csv_export/address_log_csv_exporter_test.exs @@ -4,6 +4,16 @@ defmodule Explorer.Chain.AddressLogCsvExporterTest do alias Explorer.Chain.Address alias Explorer.Chain.CSVExport.AddressLogCsvExporter + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + @third_topic_hex_string_1 "0x0000000000000000000000005079fc00f00f30000e0c8c083801cfde000008b6" + @fourth_topic_hex_string_1 "0x8c9b7729443a4444242342b2ca385a239a5c1d76a88473e1cd2ab0c70dd1b9c7" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + describe "export/3" do test "exports address logs to csv" do address = insert(:address) @@ -21,10 +31,10 @@ defmodule Explorer.Chain.AddressLogCsvExporterTest do block: transaction.block, block_number: transaction.block_number, data: "0x12", - first_topic: "0x13", - second_topic: "0x14", - third_topic: "0x15", - fourth_topic: "0x16" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + fourth_topic: topic(@fourth_topic_hex_string_1) ) from_period = Timex.format!(Timex.shift(Timex.now(), minutes: -1), "%Y-%m-%d", :strftime) diff --git a/apps/explorer/test/explorer/chain/csv_export/address_token_transfer_csv_exporter_test.exs b/apps/explorer/test/explorer/chain/csv_export/address_token_transfer_csv_exporter_test.exs index 36b62d9757b8..88eeb2982530 100644 --- a/apps/explorer/test/explorer/chain/csv_export/address_token_transfer_csv_exporter_test.exs +++ b/apps/explorer/test/explorer/chain/csv_export/address_token_transfer_csv_exporter_test.exs @@ -70,7 +70,7 @@ defmodule Explorer.Chain.AddressTokenTransferCsvExporterTest do assert result.tx_hash == to_string(transaction.hash) assert result.from_address == Address.checksum(token_transfer.from_address_hash) assert result.to_address == Address.checksum(token_transfer.to_address_hash) - assert result.timestamp == to_string(transaction.block.timestamp) + assert result.timestamp == to_string(transaction.block_timestamp) assert result.type == "OUT" end diff --git a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs index fa8d97e0a5c2..6789d37b5a8e 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs @@ -89,6 +89,13 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do value_5 = Decimal.new(2) token_id_5 = Decimal.new(555) + token_erc_404 = insert(:token, holder_count: 0) + token_erc_404_contract_address_hash = token_erc_404.contract_address_hash + value_6 = Decimal.new(10) + token_id_6 = Decimal.new(333) + + value_7 = Decimal.new(25) + block_number = 1 assert {:ok, @@ -121,6 +128,20 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do token_contract_address_hash: ^token_erc_721_contract_address_hash, value: ^value_5, token_id: nil + }, + %Explorer.Chain.Address.CurrentTokenBalance{ + address_hash: ^address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_erc_404_contract_address_hash, + value: ^value_7, + token_id: nil + }, + %Explorer.Chain.Address.CurrentTokenBalance{ + address_hash: ^address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_erc_404_contract_address_hash, + value: ^value_6, + token_id: ^token_id_6 } ], address_current_token_balances_update_token_holder_counts: [ @@ -135,6 +156,10 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do %{ contract_address_hash: ^token_erc_721_contract_address_hash, holder_count: 1 + }, + %{ + contract_address_hash: ^token_erc_404_contract_address_hash, + holder_count: 2 } ] }} = @@ -184,6 +209,24 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do value_fetched_at: DateTime.utc_now(), token_id: token_id_5, token_type: "ERC-721" + }, + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_erc_404_contract_address_hash, + value: value_6, + value_fetched_at: DateTime.utc_now(), + token_id: token_id_6, + token_type: "ERC-404" + }, + %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_erc_404_contract_address_hash, + value: value_7, + value_fetched_at: DateTime.utc_now(), + token_id: nil, + token_type: "ERC-404" } ], options @@ -197,7 +240,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do current_token_balances |> Enum.count() - assert current_token_balances_count == 4 + assert current_token_balances_count == 6 end test "updates when the new block number is greater", %{ diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index 7e79c20916f3..a317767a96cf 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -99,7 +99,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do ] }} = run_block_consensus_change(block, true, options) - assert count(Address.CurrentTokenBalance) == 0 + assert %{value: nil} = Repo.one(Address.CurrentTokenBalance) end test "delete_address_current_token_balances does not delete rows with matching block number when consensus is false", @@ -118,100 +118,6 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do assert count(Address.CurrentTokenBalance) == count end - test "derive_address_current_token_balances inserts rows if there is an address_token_balance left for the rows deleted by delete_address_current_token_balances", - %{consensus_block: %{number: block_number} = block, options: options} do - token = insert(:token) - token_contract_address_hash = token.contract_address_hash - - %Address{hash: address_hash} = - insert_address_with_token_balances(%{ - previous: %{value: 1}, - current: %{block_number: block_number, value: 2}, - token_contract_address_hash: token_contract_address_hash - }) - - # Token must exist with non-`nil` `holder_count` for `blocks_update_token_holder_counts` to update - update_holder_count!(token_contract_address_hash, 1) - - assert count(Address.TokenBalance) == 2 - assert count(Address.CurrentTokenBalance) == 1 - - previous_block_number = block_number - 1 - - insert(:block, number: block_number, consensus: true) - - assert {:ok, - %{ - delete_address_current_token_balances: [ - %{ - address_hash: ^address_hash, - token_contract_address_hash: ^token_contract_address_hash - } - ], - delete_address_token_balances: [ - %{ - address_hash: ^address_hash, - token_contract_address_hash: ^token_contract_address_hash, - block_number: ^block_number - } - ], - derive_address_current_token_balances: [ - %{ - address_hash: ^address_hash, - token_contract_address_hash: ^token_contract_address_hash, - block_number: ^previous_block_number - } - ], - # no updates because it both deletes and derives a holder - blocks_update_token_holder_counts: [] - }} = run_block_consensus_change(block, true, options) - - assert count(Address.TokenBalance) == 1 - assert count(Address.CurrentTokenBalance) == 1 - - previous_value = Decimal.new(1) - - assert %Address.CurrentTokenBalance{block_number: ^previous_block_number, value: ^previous_value} = - Repo.get_by(Address.CurrentTokenBalance, - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash - ) - end - - test "a non-holder reverting to a holder increases the holder_count", - %{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do - token = insert(:token) - token_contract_address_hash = token.contract_address_hash - - non_holder_reverts_to_holder(%{ - current: %{block_number: block_number}, - token_contract_address_hash: token_contract_address_hash - }) - - # Token must exist with non-`nil` `holder_count` for `blocks_update_token_holder_counts` to update - update_holder_count!(token_contract_address_hash, 0) - - insert(:block, number: block_number, consensus: true) - - block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true) - - %Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, block_params) - changes_list = [block_changes] - - assert {:ok, - %{ - blocks_update_token_holder_counts: [ - %{ - contract_address_hash: ^token_contract_address_hash, - holder_count: 1 - } - ] - }} = - Multi.new() - |> Blocks.run(changes_list, options) - |> Repo.transaction() - end - test "a holder reverting to a non-holder decreases the holder_count", %{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do token = insert(:token) @@ -369,6 +275,25 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do assert %{block_number: ^number, block_hash: ^hash} = Repo.one(PendingBlockOperation) end + test "inserts pending_block_operations only for actually inserted blocks", + %{consensus_block: %{miner_hash: miner_hash}, options: options} do + %{number: number, hash: hash} = new_block = params_for(:block, miner_hash: miner_hash, consensus: true) + new_block1 = params_for(:block, miner_hash: miner_hash, consensus: true) + + miner = Repo.get_by(Address, hash: miner_hash) + + insert(:block, Map.put(new_block1, :miner, miner)) + + %Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, new_block) + %Ecto.Changeset{valid?: true, changes: block_changes1} = Block.changeset(%Block{}, new_block1) + + Multi.new() + |> Blocks.run([block_changes, block_changes1], options) + |> Repo.transaction() + + assert %{block_number: ^number, block_hash: ^hash} = Repo.one(PendingBlockOperation) + end + test "change instance owner if was token transfer in older blocks", %{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do block_number = block_number + 2 @@ -386,6 +311,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do tt = insert(:token_transfer, token_ids: [id], + token_type: "ERC-721", transaction: transaction, token_contract_address: token_address, block_number: block_number, @@ -404,6 +330,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do for _ <- 0..10 do insert(:token_transfer, token_ids: [id], + token_type: "ERC-721", transaction: transaction, token_contract_address: tt.token_contract_address, block_number: consensus_block_1.number, @@ -414,6 +341,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do tt_1 = insert(:token_transfer, token_ids: [id], + token_type: "ERC-721", transaction: transaction, token_contract_address: tt.token_contract_address, block_number: consensus_block_1.number, @@ -431,6 +359,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do insert(:token_transfer, token_ids: [id], + token_type: "ERC-721", transaction: tx, token_contract_address: tt.token_contract_address, block_number: consensus_block_2.number, @@ -490,6 +419,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do tt = insert(:token_transfer, token_ids: [id], + token_type: "ERC-721", transaction: transaction, token_contract_address: token_address, block_number: block_number, diff --git a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs index ffe02c5dc7df..280fe86ea4e3 100644 --- a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs @@ -163,35 +163,38 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do internal_transaction_changes_2_1 ]) - assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction0.hash, where: i.index == 0) - |> Repo.one() - |> is_nil() + # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth + + # assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction0.hash, where: i.index == 0) + # |> Repo.one() + # |> is_nil() assert 1 == Repo.get_by!(InternalTransaction, transaction_hash: transaction0.hash, index: 1).block_index - assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction1.hash) |> Repo.one() |> is_nil() + # assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction1.hash) |> Repo.one() |> is_nil() - assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction2.hash, where: i.index == 0) - |> Repo.one() - |> is_nil() + # assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction2.hash, where: i.index == 0) + # |> Repo.one() + # |> is_nil() assert 4 == Repo.get_by!(InternalTransaction, transaction_hash: transaction2.hash, index: 1).block_index end - test "simple coin transfer has no internal transaction inserted" do - transaction = insert(:transaction) |> with_block(status: :ok) - insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) + # test "simple coin transfer has no internal transaction inserted for Nethermind" do + # transaction = insert(:transaction) |> with_block(status: :ok) + # insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) - assert :ok == transaction.status + # assert :ok == transaction.status - index = 0 + # # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth + # index = 0 - internal_transaction_changes = - make_internal_transaction_changes_for_simple_coin_transfers(transaction, index, nil) + # internal_transaction_changes = + # make_internal_transaction_changes_for_simple_coin_transfers(transaction, index, nil) - assert {:ok, _} = run_internal_transactions([internal_transaction_changes]) + # assert {:ok, _} = run_internal_transactions([internal_transaction_changes]) - assert !Repo.exists?(from(i in InternalTransaction, where: i.transaction_hash == ^transaction.hash)) - end + # assert !Repo.exists?(from(i in InternalTransaction, where: i.transaction_hash == ^transaction.hash)) + # end test "pending transactions don't get updated not its internal_transactions inserted" do transaction = insert(:transaction) |> with_block(status: :ok) @@ -283,8 +286,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do assert full_block.hash == inserted.block_hash - transaction_changes = make_internal_transaction_changes(inserted, 0, nil) - transaction_changes_2 = make_internal_transaction_changes(inserted, 1, nil) + # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth + _transaction_changes_0 = make_internal_transaction_changes(inserted, 0, nil) + transaction_changes = make_internal_transaction_changes(inserted, 1, nil) + transaction_changes_2 = make_internal_transaction_changes(inserted, 2, nil) empty_changes = make_empty_block_changes(empty_block.number) assert {:ok, _} = run_internal_transactions([empty_changes, transaction_changes, transaction_changes_2]) @@ -292,12 +297,12 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do assert %{consensus: true} = Repo.get(Block, empty_block.hash) assert PendingBlockOperation |> Repo.get(empty_block.hash) |> is_nil() - assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash, where: i.index == 0) + assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash, where: i.index == 1) |> Repo.one() |> is_nil() == - true + false - assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash, where: i.index == 1) + assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash, where: i.index == 2) |> Repo.one() |> is_nil() == false @@ -306,6 +311,29 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do assert PendingBlockOperation |> Repo.get(full_block.hash) |> is_nil() end + test "does not remove consensus from non-traceable blocks" do + original_config = Application.get_env(:indexer, :trace_block_ranges) + + full_block = insert(:block) + transaction_a = insert(:transaction) |> with_block(full_block) + transaction_b = insert(:transaction) |> with_block(full_block) + + Application.put_env(:indexer, :trace_block_ranges, "#{full_block.number + 1}..latest") + + insert(:pending_block_operation, block_hash: full_block.hash, block_number: full_block.number) + + transaction_a_changes = make_internal_transaction_changes(transaction_a, 0, nil) + + assert {:ok, _} = run_internal_transactions([transaction_a_changes]) + + assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction_a.hash) |> Repo.one() |> is_nil() + assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction_b.hash) |> Repo.one() |> is_nil() + + assert %{consensus: true} = Repo.get(Block, full_block.hash) + + on_exit(fn -> Application.put_env(:indexer, :trace_block_ranges, original_config) end) + end + test "successfully imports internal transaction with stop type" do block = insert(:block) transaction = insert(:transaction) |> with_block(block, status: :ok) @@ -381,7 +409,12 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do to_address_hash: insert(:address).hash, call_type: :call, gas: 0, - gas_used: nil, + gas_used: + if is_nil(error) do + 100_500 + else + nil + end, input: %Data{bytes: <<>>}, output: if is_nil(error) do diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index 322ca7a7ff80..484aca9f17c5 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -22,9 +22,16 @@ defmodule Explorer.Chain.ImportTest do @moduletag :capturelog + @first_topic_hex_string "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @second_topic_hex_string "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + @third_topic_hex_string "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d" + doctest Import describe "all/1" do + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string) + {:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string) + {:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string) # set :timeout options to cover lines that use the timeout override when available @import_data %{ blocks: %{ @@ -53,7 +60,8 @@ defmodule Explorer.Chain.ImportTest do block_number: 37, transaction_index: 0, transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - index: 0, + # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth + index: 1, trace_address: [], type: "call", call_type: "call", @@ -69,7 +77,7 @@ defmodule Explorer.Chain.ImportTest do block_number: 37, transaction_index: 1, transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - index: 1, + index: 2, trace_address: [0], type: "call", call_type: "call", @@ -91,13 +99,12 @@ defmodule Explorer.Chain.ImportTest do block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, fourth_topic: nil, index: 0, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } ], timeout: 5 @@ -157,6 +164,7 @@ defmodule Explorer.Chain.ImportTest do from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + token_type: "ERC-20", transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } ], @@ -165,6 +173,9 @@ defmodule Explorer.Chain.ImportTest do } test "with valid data" do + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string) + {:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string) + {:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string) difficulty = Decimal.new(340_282_366_920_938_463_463_374_607_431_768_211_454) total_difficulty = Decimal.new(12_590_447_576_074_723_148_144_860_474_975_121_280_509) token_transfer_amount = Decimal.new(1_000_000_000_000_000_000) @@ -260,6 +271,15 @@ defmodule Explorer.Chain.ImportTest do <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> } + }, + %{ + index: 2, + transaction_hash: %Hash{ + byte_count: 32, + bytes: + <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, + 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> + } } ], logs: [ @@ -276,9 +296,9 @@ defmodule Explorer.Chain.ImportTest do 167, 100, 0, 0>> }, index: 0, - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: ^first_topic, + second_topic: ^second_topic, + third_topic: ^third_topic, fourth_topic: nil, transaction_hash: %Hash{ byte_count: 32, @@ -286,7 +306,6 @@ defmodule Explorer.Chain.ImportTest do <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> }, - type: "mined", inserted_at: %{}, updated_at: %{} } @@ -344,7 +363,8 @@ defmodule Explorer.Chain.ImportTest do 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> }, inserted_at: %{}, - updated_at: %{} + updated_at: %{}, + token_type: "ERC-20" } ] }} = Import.all(@import_data) @@ -494,7 +514,8 @@ defmodule Explorer.Chain.ImportTest do Subscriber.to(:internal_transactions, :realtime) Import.all(@import_data) - assert_receive {:chain_event, :internal_transactions, :realtime, [%{transaction_hash: _, index: _}]} + assert_receive {:chain_event, :internal_transactions, :realtime, + [%{transaction_hash: _, index: _}, %{transaction_hash: _, index: _}]} end test "publishes transactions data to subscribers on insert" do @@ -2123,94 +2144,7 @@ defmodule Explorer.Chain.ImportTest do } }) - assert is_nil( - Repo.get_by(Address.CurrentTokenBalance, - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash - ) - ) - - assert is_nil( - Repo.get_by(Address.TokenBalance, - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number - ) - ) - end - - test "address_current_token_balances is derived during reorgs" do - %Block{number: block_number} = insert(:block, consensus: true) - previous_block_number = block_number - 1 - - %Address.TokenBalance{ - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash, - value: previous_value, - block_number: previous_block_number - } = insert(:token_balance, block_number: previous_block_number) - - address = Repo.get(Address, address_hash) - - %Address.TokenBalance{ - address_hash: ^address_hash, - token_contract_address_hash: token_contract_address_hash, - value: current_value, - block_number: ^block_number - } = - insert(:token_balance, - address: address, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number - ) - - refute current_value == previous_value - - %Address.CurrentTokenBalance{ - address_hash: ^address_hash, - token_contract_address_hash: ^token_contract_address_hash, - block_number: ^block_number - } = - insert(:address_current_token_balance, - address: address, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number, - value: current_value - ) - - miner_hash_after = address_hash() - from_address_hash_after = address_hash() - block_hash_after = block_hash() - - assert {:ok, _} = - Import.all(%{ - addresses: %{ - params: [ - %{hash: miner_hash_after}, - %{hash: from_address_hash_after} - ] - }, - blocks: %{ - params: [ - %{ - consensus: true, - difficulty: 1, - gas_limit: 1, - gas_used: 1, - hash: block_hash_after, - miner_hash: miner_hash_after, - nonce: 1, - number: block_number, - parent_hash: block_hash(), - size: 1, - timestamp: Timex.parse!("2019-01-01T02:00:00Z", "{ISO:Extended:Z}"), - total_difficulty: 1 - } - ] - } - }) - - assert %Address.CurrentTokenBalance{block_number: ^previous_block_number, value: ^previous_value} = + assert %{value: nil} = Repo.get_by(Address.CurrentTokenBalance, address_hash: address_hash, token_contract_address_hash: token_contract_address_hash @@ -2224,100 +2158,5 @@ defmodule Explorer.Chain.ImportTest do ) ) end - - test "address_token_balances and address_current_token_balances can be replaced during reorgs" do - %Block{number: block_number} = insert(:block, consensus: true) - value_before = Decimal.new(1) - - %Address{hash: address_hash} = address = insert(:address) - - %Address.TokenBalance{ - address_hash: ^address_hash, - token_contract_address_hash: token_contract_address_hash, - block_number: ^block_number - } = insert(:token_balance, address: address, block_number: block_number, value: value_before) - - %Address.CurrentTokenBalance{ - address_hash: ^address_hash, - token_contract_address_hash: ^token_contract_address_hash, - block_number: ^block_number - } = - insert(:address_current_token_balance, - address: address, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number, - value: value_before - ) - - miner_hash_after = address_hash() - from_address_hash_after = address_hash() - block_hash_after = block_hash() - value_after = Decimal.add(value_before, 1) - - assert {:ok, _} = - Import.all(%{ - addresses: %{ - params: [ - %{hash: address_hash}, - %{hash: token_contract_address_hash}, - %{hash: miner_hash_after}, - %{hash: from_address_hash_after} - ] - }, - address_token_balances: %{ - params: [ - %{ - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number, - value: value_after, - token_type: "ERC-20" - } - ] - }, - address_current_token_balances: %{ - params: [ - %{ - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number, - value: value_after, - token_type: "ERC-20" - } - ] - }, - blocks: %{ - params: [ - %{ - consensus: true, - difficulty: 1, - gas_limit: 1, - gas_used: 1, - hash: block_hash_after, - miner_hash: miner_hash_after, - nonce: 1, - number: block_number, - parent_hash: block_hash(), - size: 1, - timestamp: Timex.parse!("2019-01-01T02:00:00Z", "{ISO:Extended:Z}"), - total_difficulty: 1 - } - ] - } - }) - - assert %Address.CurrentTokenBalance{value: ^value_after} = - Repo.get_by(Address.CurrentTokenBalance, - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash - ) - - assert %Address.TokenBalance{value: ^value_after} = - Repo.get_by(Address.TokenBalance, - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number - ) - end end end diff --git a/apps/explorer/test/explorer/chain/log_test.exs b/apps/explorer/test/explorer/chain/log_test.exs index 1b5df9388e9c..87881776de29 100644 --- a/apps/explorer/test/explorer/chain/log_test.exs +++ b/apps/explorer/test/explorer/chain/log_test.exs @@ -7,10 +7,19 @@ defmodule Explorer.Chain.LogTest do alias Explorer.Chain.{Log, SmartContract} alias Explorer.Repo + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + setup :set_mox_from_context doctest Log + setup :verify_on_exit! + describe "changeset/2" do test "accepts valid attributes" do params = @@ -34,18 +43,21 @@ defmodule Explorer.Chain.LogTest do params_for( :log, address_hash: build(:address).hash, - first_topic: "ham", + first_topic: @first_topic_hex_string_1, transaction_hash: build(:transaction).hash, block_hash: build(:block).hash ) - assert %Changeset{changes: %{first_topic: "ham"}, valid?: true} = Log.changeset(%Log{}, params) + result = Log.changeset(%Log{}, params) + + assert result.valid? == true + assert result.changes.first_topic == topic(@first_topic_hex_string_1) end test "assigns optional attributes" do - params = Map.put(params_for(:log), :first_topic, "ham") + params = Map.put(params_for(:log), :first_topic, topic(@first_topic_hex_string_1)) changeset = Log.changeset(%Log{}, params) - assert changeset.changes.first_topic === "ham" + assert changeset.changes.first_topic === topic(@first_topic_hex_string_1) end end @@ -97,14 +109,14 @@ defmodule Explorer.Chain.LogTest do insert(:log, address: to_address, transaction: transaction, - first_topic: topic1, - second_topic: topic2, - third_topic: topic3, + first_topic: topic(topic1), + second_topic: topic(topic2), + third_topic: topic(topic3), fourth_topic: nil, data: data ) - get_eip1967_implementation() + request_zero_implementations() assert {{:ok, "eb9b3c4c", "WantsPets(string indexed _from_human, uint256 _number, bool indexed _belly)", [ @@ -151,9 +163,9 @@ defmodule Explorer.Chain.LogTest do log = insert(:log, transaction: transaction, - first_topic: topic1, - second_topic: topic2, - third_topic: topic3, + first_topic: topic(topic1), + second_topic: topic(topic2), + third_topic: topic(topic3), fourth_topic: nil, data: data ) @@ -173,7 +185,7 @@ defmodule Explorer.Chain.LogTest do end end - def get_eip1967_implementation do + def request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -211,5 +223,17 @@ defmodule Explorer.Chain.LogTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) end end diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs new file mode 100644 index 000000000000..c61b10c795a4 --- /dev/null +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs @@ -0,0 +1,566 @@ +defmodule Explorer.Chain.SmartContract.ProxyTest do + use Explorer.DataCase, async: false + import Mox + alias Explorer.Chain.SmartContract + alias Explorer.Chain.SmartContract.Proxy + + setup :verify_on_exit! + setup :set_mox_global + + describe "proxy contracts features" do + @proxy_abi [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "bool", "name" => ""}], + "name" => "upgradeTo", + "inputs" => [%{"type" => "address", "name" => "newImplementation"}], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "version", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "renounceOwnership", + "inputs" => [], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getOwner", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getProxyStorage", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferOwnership", + "inputs" => [%{"type" => "address", "name" => "_newOwner"}], + "constant" => false + }, + %{ + "type" => "constructor", + "stateMutability" => "nonpayable", + "payable" => false, + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{"type" => "fallback", "stateMutability" => "nonpayable", "payable" => false}, + %{ + "type" => "event", + "name" => "Upgraded", + "inputs" => [ + %{"type" => "uint256", "name" => "version", "indexed" => false}, + %{"type" => "address", "name" => "implementation", "indexed" => true} + ], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipRenounced", + "inputs" => [%{"type" => "address", "name" => "previousOwner", "indexed" => true}], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipTransferred", + "inputs" => [ + %{"type" => "address", "name" => "previousOwner", "indexed" => true}, + %{"type" => "address", "name" => "newOwner", "indexed" => true} + ], + "anonymous" => false + } + ] + + @implementation_abi [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + + defp request_EIP1967_zero_implementations do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + # EIP-1967 + EIP-1822 + defp request_zero_implementations do + request_EIP1967_zero_implementations() + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + test "combine_proxy_implementation_abi/2 returns empty [] abi if proxy abi is null" do + proxy_contract_address = insert(:contract_address) + + assert Proxy.combine_proxy_implementation_abi(%SmartContract{address_hash: proxy_contract_address.hash, abi: nil}) == + [] + end + + test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + request_zero_implementations() + + assert Proxy.combine_proxy_implementation_abi(smart_contract) == [] + end + + test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + request_zero_implementations() + + assert Proxy.combine_proxy_implementation_abi(smart_contract) == @proxy_abi + end + + test "combine_proxy_implementation_abi/2 returns proxy + implementation abi if implementation is verified" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + request_zero_implementations() + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000" <> implementation_contract_address_hash_string + } + ]} + end + ) + + combined_abi = Proxy.combine_proxy_implementation_abi(smart_contract) + + assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == false + assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == false + assert Enum.any?(combined_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == true + assert Enum.any?(combined_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == true + end + + test "get_implementation_abi_from_proxy/2 returns empty [] abi if proxy abi is null" do + proxy_contract_address = insert(:contract_address) + + assert Proxy.get_implementation_abi_from_proxy( + %SmartContract{address_hash: proxy_contract_address.hash, abi: nil}, + [] + ) == + [] + end + + test "get_implementation_abi_from_proxy/2 returns [] abi for unverified proxy" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + request_zero_implementations() + + assert Proxy.combine_proxy_implementation_abi(smart_contract) == [] + end + + test "get_implementation_abi_from_proxy/2 returns [] if implementation is not verified" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + request_zero_implementations() + + assert Proxy.get_implementation_abi_from_proxy(smart_contract, []) == [] + end + + test "get_implementation_abi_from_proxy/2 returns implementation abi if implementation is verified" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + request_zero_implementations() + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000" <> implementation_contract_address_hash_string + } + ]} + end + ) + + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) + + assert implementation_abi == @implementation_abi + end + + test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern (logic contract)" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn %{ + id: _id, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000" <> implementation_contract_address_hash_string} + end + ) + + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) + + assert implementation_abi == @implementation_abi + end + end + + @beacon_abi [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "implementation", + "inputs" => [] + } + ] + test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern (beacon contract)" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + beacon_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: beacon_contract_address.hash, + abi: @beacon_abi, + contract_code_md5: "123" + ) + + beacon_contract_address_hash_string = Base.encode16(beacon_contract_address.hash.bytes, case: :lower) + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + eip_1967_beacon_proxy_mock_requests( + beacon_contract_address_hash_string, + implementation_contract_address_hash_string, + :full_32 + ) + + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) + verify!(EthereumJSONRPC.Mox) + + assert implementation_abi == @implementation_abi + end + + test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern (beacon contract) when eth_getStorageAt returns 20 bytes address" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + beacon_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: beacon_contract_address.hash, + abi: @beacon_abi, + contract_code_md5: "123" + ) + + beacon_contract_address_hash_string = Base.encode16(beacon_contract_address.hash.bytes, case: :lower) + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + eip_1967_beacon_proxy_mock_requests( + beacon_contract_address_hash_string, + implementation_contract_address_hash_string, + :exact_20 + ) + + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) + verify!(EthereumJSONRPC.Mox) + + assert implementation_abi == @implementation_abi + end + + test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern (beacon contract) when eth_getStorageAt returns less 20 bytes address" do + proxy_contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + beacon_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: beacon_contract_address.hash, + abi: @beacon_abi, + contract_code_md5: "123" + ) + + beacon_contract_address_hash_string = Base.encode16(beacon_contract_address.hash.bytes, case: :lower) + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @implementation_abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + eip_1967_beacon_proxy_mock_requests( + beacon_contract_address_hash_string, + implementation_contract_address_hash_string, + :short + ) + + implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) + verify!(EthereumJSONRPC.Mox) + + assert implementation_abi == @implementation_abi + end + + defp eip_1967_beacon_proxy_mock_requests( + beacon_contract_address_hash_string, + implementation_contract_address_hash_string, + mode + ) do + response = + case mode do + :full_32 -> "0x000000000000000000000000" <> beacon_contract_address_hash_string + :exact_20 -> "0x" <> beacon_contract_address_hash_string + :short -> "0x" <> String.slice(beacon_contract_address_hash_string, 10..-1) + end + + EthereumJSONRPC.Mox + |> expect( + :json_rpc, + fn %{ + id: _id, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end + ) + |> expect( + :json_rpc, + fn %{ + id: _id, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, response} + end + ) + |> expect( + :json_rpc, + fn [ + %{ + id: _id, + method: "eth_call", + params: [ + %{data: "0x5c60da1b", to: "0x" <> ^beacon_contract_address_hash_string}, + "latest" + ] + } + ], + _options -> + { + :ok, + [ + %{ + id: _id, + jsonrpc: "2.0", + result: "0x000000000000000000000000" <> implementation_contract_address_hash_string + } + ] + } + end + ) + end +end diff --git a/apps/explorer/test/explorer/chain/smart_contract_test.exs b/apps/explorer/test/explorer/chain/smart_contract_test.exs index aa2b90cec982..f0e5b2075e3f 100644 --- a/apps/explorer/test/explorer/chain/smart_contract_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract_test.exs @@ -3,7 +3,8 @@ defmodule Explorer.Chain.SmartContractTest do import Mox alias Explorer.Chain - alias Explorer.Chain.SmartContract + alias Explorer.Chain.{Address, SmartContract} + alias Explorer.Chain.SmartContract.Proxy doctest Explorer.Chain.SmartContract @@ -14,47 +15,65 @@ defmodule Explorer.Chain.SmartContractTest do test "check proxy_contract/1 function" do smart_contract = insert(:smart_contract) - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) - Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20)) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) refute smart_contract.implementation_fetched_at - # fetch nil implementation and save it to db + # fetch nil implementation and don't save it to db get_eip1967_implementation_zero_addresses() - refute SmartContract.proxy_contract?(smart_contract) - verify!(EthereumJSONRPC.Mox) - assert_empty_implementation(smart_contract.address_hash) - # extract proxy info from db - refute SmartContract.proxy_contract?(smart_contract) + refute Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) - assert_empty_implementation(smart_contract.address_hash) + assert_implementation_never_fetched(smart_contract.address_hash) + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, 0) - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) + Application.put_env(:explorer, :proxy, proxy) get_eip1967_implementation_error_response() - refute SmartContract.proxy_contract?(smart_contract) + refute Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) get_eip1967_implementation_non_zero_address() - assert SmartContract.proxy_contract?(smart_contract) + assert Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) assert_implementation_address(smart_contract.address_hash) get_eip1967_implementation_non_zero_address() - assert SmartContract.proxy_contract?(smart_contract) + assert Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) assert_implementation_address(smart_contract.address_hash) - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) - assert SmartContract.proxy_contract?(smart_contract) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) + + assert Proxy.proxy_contract?(smart_contract) + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, 0) + + Application.put_env(:explorer, :proxy, proxy) - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) get_eip1967_implementation_non_zero_address() - assert SmartContract.proxy_contract?(smart_contract) + assert Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) get_eip1967_implementation_error_response() - assert SmartContract.proxy_contract?(smart_contract) + assert Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) end @@ -62,35 +81,44 @@ defmodule Explorer.Chain.SmartContractTest do smart_contract = insert(:smart_contract) implementation_smart_contract = insert(:smart_contract, name: "proxy") - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) - Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20)) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) refute smart_contract.implementation_fetched_at - # fetch nil implementation and save it to db + # fetch nil implementation and don't save it to db get_eip1967_implementation_zero_addresses() assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract) verify!(EthereumJSONRPC.Mox) - assert_empty_implementation(smart_contract.address_hash) + assert_implementation_never_fetched(smart_contract.address_hash) # extract proxy info from db - assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract) - assert_empty_implementation(smart_contract.address_hash) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, 0) - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) + Application.put_env(:explorer, :proxy, proxy) string_implementation_address_hash = to_string(implementation_smart_contract.address_hash) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> + mock_empty_logic_storage_pointer_request() + |> mock_empty_beacon_storage_pointer_request() + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> {:ok, string_implementation_address_hash} end) @@ -118,23 +146,38 @@ defmodule Explorer.Chain.SmartContractTest do implementation_smart_contract.name ) - contract_1 = Chain.address_hash_to_smart_contract(smart_contract.address_hash) + contract_1 = SmartContract.address_hash_to_smart_contract(smart_contract.address_hash) + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) + Application.put_env(:explorer, :proxy, proxy) assert {^string_implementation_address_hash, "proxy"} = SmartContract.get_implementation_address_hash(smart_contract) - contract_2 = Chain.address_hash_to_smart_contract(smart_contract.address_hash) + contract_2 = SmartContract.address_hash_to_smart_contract(smart_contract.address_hash) assert contract_1.implementation_fetched_at == contract_2.implementation_fetched_at && contract_1.updated_at == contract_2.updated_at - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, 0) + + Application.put_env(:explorer, :proxy, proxy) get_eip1967_implementation_zero_addresses() - assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract) + + assert {^string_implementation_address_hash, "proxy"} = + SmartContract.get_implementation_address_hash(smart_contract) + verify!(EthereumJSONRPC.Mox) - assert_empty_implementation(smart_contract.address_hash) + + assert contract_1.implementation_fetched_at == contract_2.implementation_fetched_at && + contract_1.updated_at == contract_2.updated_at end test "test get_implementation_address_hash/1 for twins contract" do @@ -143,11 +186,16 @@ defmodule Explorer.Chain.SmartContractTest do smart_contract = insert(:smart_contract) another_address = insert(:contract_address) - twin = Chain.address_hash_to_smart_contract(another_address.hash) + twin = SmartContract.address_hash_to_smart_contract(another_address.hash) implementation_smart_contract = insert(:smart_contract, name: "proxy") - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) - Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20)) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) # fetch nil implementation get_eip1967_implementation_zero_addresses() @@ -162,18 +210,7 @@ defmodule Explorer.Chain.SmartContractTest do string_implementation_address_hash = to_string(implementation_smart_contract.address_hash) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, string_implementation_address_hash} - end) + expect_address_in_response(string_implementation_address_hash) assert {^string_implementation_address_hash, "proxy"} = SmartContract.get_implementation_address_hash(twin) @@ -194,8 +231,13 @@ defmodule Explorer.Chain.SmartContractTest do implementation_smart_contract = insert(:smart_contract, name: "proxy") - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) - Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20)) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) # fetch nil implementation get_eip1967_implementation_zero_addresses() @@ -210,18 +252,7 @@ defmodule Explorer.Chain.SmartContractTest do string_implementation_address_hash = to_string(implementation_smart_contract.address_hash) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, string_implementation_address_hash} - end) + expect_address_in_response(string_implementation_address_hash) assert {^string_implementation_address_hash, "proxy"} = SmartContract.get_implementation_address_hash(twin) @@ -252,8 +283,13 @@ defmodule Explorer.Chain.SmartContractTest do implementation_smart_contract = insert(:smart_contract, name: "proxy") - Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) - Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20)) + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) # fetch nil implementation get_eip1967_implementation_zero_addresses() @@ -268,18 +304,7 @@ defmodule Explorer.Chain.SmartContractTest do string_implementation_address_hash = to_string(implementation_smart_contract.address_hash) - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, string_implementation_address_hash} - end) + expect_address_in_response(string_implementation_address_hash) assert {^string_implementation_address_hash, "proxy"} = SmartContract.get_implementation_address_hash(twin) @@ -298,31 +323,15 @@ defmodule Explorer.Chain.SmartContractTest do end def get_eip1967_implementation_zero_addresses do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} - end) - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", - "latest" - ] - }, - _options -> - {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} - end) + mock_empty_logic_storage_pointer_request() + |> mock_empty_beacon_storage_pointer_request() + |> mock_empty_oz_storage_pointer_request() + |> mock_empty_eip_1822_storage_pointer_request() + end + + def get_eip1967_implementation_non_zero_address do + mock_empty_logic_storage_pointer_request() + |> mock_empty_beacon_storage_pointer_request() |> expect(:json_rpc, fn %{ id: 0, method: "eth_getStorageAt", @@ -333,21 +342,6 @@ defmodule Explorer.Chain.SmartContractTest do ] }, _options -> - {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} - end) - end - - def get_eip1967_implementation_non_zero_address do - expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000001"} end) end @@ -366,38 +360,541 @@ defmodule Explorer.Chain.SmartContractTest do _options -> {:error, "error"} end) + |> mock_empty_beacon_storage_pointer_request() + |> mock_empty_oz_storage_pointer_request() + |> mock_empty_eip_1822_storage_pointer_request() end def assert_empty_implementation(address_hash) do - contract = Chain.address_hash_to_smart_contract(address_hash) + contract = SmartContract.address_hash_to_smart_contract(address_hash) assert contract.implementation_fetched_at refute contract.implementation_name refute contract.implementation_address_hash end def assert_implementation_never_fetched(address_hash) do - contract = Chain.address_hash_to_smart_contract(address_hash) + contract = SmartContract.address_hash_to_smart_contract(address_hash) refute contract.implementation_fetched_at refute contract.implementation_name refute contract.implementation_address_hash end def assert_implementation_address(address_hash) do - contract = Chain.address_hash_to_smart_contract(address_hash) + contract = SmartContract.address_hash_to_smart_contract(address_hash) assert contract.implementation_fetched_at assert contract.implementation_address_hash end def assert_implementation_name(address_hash) do - contract = Chain.address_hash_to_smart_contract(address_hash) + contract = SmartContract.address_hash_to_smart_contract(address_hash) assert contract.implementation_fetched_at assert contract.implementation_name end def assert_exact_name_and_address(address_hash, implementation_address_hash, implementation_name) do - contract = Chain.address_hash_to_smart_contract(address_hash) + contract = SmartContract.address_hash_to_smart_contract(address_hash) assert contract.implementation_fetched_at assert contract.implementation_name == implementation_name assert to_string(contract.implementation_address_hash) == to_string(implementation_address_hash) end + + describe "create_smart_contract/1" do + setup do + smart_contract_bytecode = + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" + + created_contract_address = + insert( + :address, + hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + contract_code: smart_contract_bytecode + ) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :internal_transaction_create, + transaction: transaction, + index: 0, + created_contract_address: created_contract_address, + created_contract_code: smart_contract_bytecode, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + valid_attrs = %{ + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + name: "SimpleStorage", + compiler_version: "0.4.23", + optimization: false, + contract_source_code: + "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + } + + {:ok, valid_attrs: valid_attrs, address: created_contract_address} + end + + test "with valid data creates a smart contract", %{valid_attrs: valid_attrs} do + assert {:ok, %SmartContract{} = smart_contract} = SmartContract.create_smart_contract(valid_attrs) + assert smart_contract.name == "SimpleStorage" + assert smart_contract.compiler_version == "0.4.23" + assert smart_contract.optimization == false + assert smart_contract.contract_source_code != "" + assert smart_contract.abi != "" + + assert Repo.get_by( + Address.Name, + address_hash: smart_contract.address_hash, + name: smart_contract.name, + primary: true + ) + end + + test "clears an existing primary name and sets the new one", %{valid_attrs: valid_attrs, address: address} do + insert(:address_name, address: address, primary: true) + assert {:ok, %SmartContract{} = smart_contract} = SmartContract.create_smart_contract(valid_attrs) + + assert Repo.get_by( + Address.Name, + address_hash: smart_contract.address_hash, + name: smart_contract.name, + primary: true + ) + end + + test "trims whitespace from address name", %{valid_attrs: valid_attrs} do + attrs = %{valid_attrs | name: " SimpleStorage "} + assert {:ok, _} = SmartContract.create_smart_contract(attrs) + assert Repo.get_by(Address.Name, name: "SimpleStorage") + end + + test "sets the address verified field to true", %{valid_attrs: valid_attrs} do + assert {:ok, %SmartContract{} = smart_contract} = SmartContract.create_smart_contract(valid_attrs) + + assert Repo.get_by(Address, hash: smart_contract.address_hash).verified == true + end + end + + describe "update_smart_contract/1" do + setup do + smart_contract_bytecode = + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" + + created_contract_address = + insert( + :address, + hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + contract_code: smart_contract_bytecode + ) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :internal_transaction_create, + transaction: transaction, + index: 0, + created_contract_address: created_contract_address, + created_contract_code: smart_contract_bytecode, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + valid_attrs = %{ + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + name: "SimpleStorage", + compiler_version: "0.4.23", + optimization: false, + contract_source_code: + "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + partially_verified: true + } + + secondary_sources = [ + %{ + file_name: "storage.sol", + contract_source_code: + "pragma solidity >=0.7.0 <0.9.0;contract Storage {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" + }, + %{ + file_name: "storage_1.sol", + contract_source_code: + "pragma solidity >=0.7.0 <0.9.0;contract Storage_1 {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" + } + ] + + changed_sources = [ + %{ + file_name: "storage_2.sol", + contract_source_code: + "pragma solidity >=0.7.0 <0.9.0;contract Storage_2 {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" + }, + %{ + file_name: "storage_3.sol", + contract_source_code: + "pragma solidity >=0.7.0 <0.9.0;contract Storage_3 {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" + } + ] + + _ = SmartContract.create_smart_contract(valid_attrs, [], secondary_sources) + + {:ok, + valid_attrs: valid_attrs, + address: created_contract_address, + secondary_sources: secondary_sources, + changed_sources: changed_sources} + end + + test "change partially_verified field", %{valid_attrs: valid_attrs, address: address} do + sc_before_call = Repo.get_by(SmartContract, address_hash: address.hash) + assert sc_before_call.name == Map.get(valid_attrs, :name) + assert sc_before_call.partially_verified == Map.get(valid_attrs, :partially_verified) + + assert {:ok, %SmartContract{}} = + SmartContract.update_smart_contract(%{ + address_hash: address.hash, + partially_verified: false, + contract_source_code: "new code" + }) + + sc_after_call = Repo.get_by(SmartContract, address_hash: address.hash) + assert sc_after_call.name == Map.get(valid_attrs, :name) + assert sc_after_call.partially_verified == false + assert sc_after_call.compiler_version == Map.get(valid_attrs, :compiler_version) + assert sc_after_call.optimization == Map.get(valid_attrs, :optimization) + assert sc_after_call.contract_source_code == "new code" + end + + test "check nothing changed", %{valid_attrs: valid_attrs, address: address} do + sc_before_call = Repo.get_by(SmartContract, address_hash: address.hash) + assert sc_before_call.name == Map.get(valid_attrs, :name) + assert sc_before_call.partially_verified == Map.get(valid_attrs, :partially_verified) + + assert {:ok, %SmartContract{}} = SmartContract.update_smart_contract(%{address_hash: address.hash}) + + sc_after_call = Repo.get_by(SmartContract, address_hash: address.hash) + assert sc_after_call.name == Map.get(valid_attrs, :name) + assert sc_after_call.partially_verified == Map.get(valid_attrs, :partially_verified) + assert sc_after_call.compiler_version == Map.get(valid_attrs, :compiler_version) + assert sc_after_call.optimization == Map.get(valid_attrs, :optimization) + assert sc_after_call.contract_source_code == Map.get(valid_attrs, :contract_source_code) + end + + test "check additional sources update", %{ + address: address, + secondary_sources: secondary_sources, + changed_sources: changed_sources + } do + sc_before_call = + Repo.get_by(Address, hash: address.hash) |> Repo.preload(smart_contract: :smart_contract_additional_sources) + + assert sc_before_call.smart_contract.smart_contract_additional_sources + |> Enum.with_index() + |> Enum.all?(fn {el, ind} -> + {:ok, src} = Enum.fetch(secondary_sources, ind) + + el.file_name == Map.get(src, :file_name) and + el.contract_source_code == Map.get(src, :contract_source_code) + end) + + assert {:ok, %SmartContract{}} = + SmartContract.update_smart_contract(%{address_hash: address.hash}, [], changed_sources) + + sc_after_call = + Repo.get_by(Address, hash: address.hash) |> Repo.preload(smart_contract: :smart_contract_additional_sources) + + assert sc_after_call.smart_contract.smart_contract_additional_sources + |> Enum.with_index() + |> Enum.all?(fn {el, ind} -> + {:ok, src} = Enum.fetch(changed_sources, ind) + + el.file_name == Map.get(src, :file_name) and + el.contract_source_code == Map.get(src, :contract_source_code) + end) + end + end + + test "get_smart_contract_abi/1 returns empty [] abi if implementation address is null" do + assert SmartContract.get_smart_contract_abi(nil) == [] + end + + test "get_smart_contract_abi/1 returns [] if implementation is not verified" do + implementation_contract_address = insert(:contract_address) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + assert SmartContract.get_smart_contract_abi("0x" <> implementation_contract_address_hash_string) == [] + end + + @abi [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + + @proxy_abi [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "bool", "name" => ""}], + "name" => "upgradeTo", + "inputs" => [%{"type" => "address", "name" => "newImplementation"}], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "version", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "renounceOwnership", + "inputs" => [], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getOwner", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getProxyStorage", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferOwnership", + "inputs" => [%{"type" => "address", "name" => "_newOwner"}], + "constant" => false + }, + %{ + "type" => "constructor", + "stateMutability" => "nonpayable", + "payable" => false, + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{"type" => "fallback", "stateMutability" => "nonpayable", "payable" => false}, + %{ + "type" => "event", + "name" => "Upgraded", + "inputs" => [ + %{"type" => "uint256", "name" => "version", "indexed" => false}, + %{"type" => "address", "name" => "implementation", "indexed" => true} + ], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipRenounced", + "inputs" => [%{"type" => "address", "name" => "previousOwner", "indexed" => true}], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipTransferred", + "inputs" => [ + %{"type" => "address", "name" => "previousOwner", "indexed" => true}, + %{"type" => "address", "name" => "newOwner", "indexed" => true} + ], + "anonymous" => false + } + ] + + test "get_smart_contract_abi/1 returns implementation abi if implementation is verified" do + proxy_contract_address = insert(:contract_address) + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + implementation_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: implementation_contract_address.hash, + abi: @abi, + contract_code_md5: "123" + ) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + implementation_abi = SmartContract.get_smart_contract_abi("0x" <> implementation_contract_address_hash_string) + + assert implementation_abi == @abi + end + + defp expect_address_in_response(string_implementation_address_hash) do + mock_empty_logic_storage_pointer_request() + |> mock_empty_beacon_storage_pointer_request() + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, string_implementation_address_hash} + end) + end + + defp mock_empty_logic_storage_pointer_request do + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + defp mock_empty_beacon_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + defp mock_empty_eip_1822_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + defp mock_empty_oz_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end end diff --git a/apps/explorer/test/explorer/chain/supply/rsk_test.exs b/apps/explorer/test/explorer/chain/supply/rsk_test.exs index a2e59294d221..5592953d5f3e 100644 --- a/apps/explorer/test/explorer/chain/supply/rsk_test.exs +++ b/apps/explorer/test/explorer/chain/supply/rsk_test.exs @@ -9,6 +9,8 @@ defmodule Explorer.Chain.Supply.RSKTest do @coin_address "0x0000000000000000000000000000000001000006" @mult 1_000_000_000_000_000_000 + setup :verify_on_exit! + test "total is 21_000_000" do assert Decimal.equal?(RSK.total(), Decimal.new(21_000_000)) end diff --git a/apps/explorer/test/explorer/chain/token/instance_test.exs b/apps/explorer/test/explorer/chain/token/instance_test.exs new file mode 100644 index 000000000000..96f78e79fcc6 --- /dev/null +++ b/apps/explorer/test/explorer/chain/token/instance_test.exs @@ -0,0 +1,83 @@ +defmodule Explorer.Chain.Token.InstanceTest do + use Explorer.DataCase + + alias Explorer.Repo + alias Explorer.Chain.Token.Instance + + describe "stream_not_inserted_token_instances/2" do + test "reduces with given reducer and accumulator for ERC-721 token" do + token_contract_address = insert(:contract_address) + token = insert(:token, contract_address: token_contract_address, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + token_transfer = + insert( + :token_transfer, + block_number: 1000, + to_address: build(:address), + transaction: transaction, + token_contract_address: token_contract_address, + token: token, + token_ids: [11] + ) + + assert [result] = 5 |> Instance.not_inserted_token_instances_query() |> Repo.all() + assert result.token_id == List.first(token_transfer.token_ids) + assert result.contract_address_hash == token_transfer.token_contract_address_hash + end + + test "does not fetch token transfers without token_ids" do + token_contract_address = insert(:contract_address) + token = insert(:token, contract_address: token_contract_address, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + insert( + :token_transfer, + block_number: 1000, + to_address: build(:address), + transaction: transaction, + token_contract_address: token_contract_address, + token: token, + token_ids: nil + ) + + assert [] = 5 |> Instance.not_inserted_token_instances_query() |> Repo.all() + end + + test "do not fetch records with token instances" do + token_contract_address = insert(:contract_address) + token = insert(:token, contract_address: token_contract_address, type: "ERC-721") + + transaction = + :transaction + |> insert() + |> with_block(insert(:block, number: 1)) + + token_transfer = + insert( + :token_transfer, + block_number: 1000, + to_address: build(:address), + transaction: transaction, + token_contract_address: token_contract_address, + token: token, + token_ids: [11] + ) + + insert(:token_instance, + token_id: List.first(token_transfer.token_ids), + token_contract_address_hash: token_transfer.token_contract_address_hash + ) + + assert [] = 5 |> Instance.not_inserted_token_instances_query() |> Repo.all() + end + end +end diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs index 29165457b773..3139f318cb39 100644 --- a/apps/explorer/test/explorer/chain/token_transfer_test.exs +++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs @@ -325,4 +325,28 @@ defmodule Explorer.Chain.TokenTransferTest do assert Enum.member?(page_two, transaction_one_bytes) == true end end + + describe "uncataloged_token_transfer_block_numbers/0" do + test "returns a list of block numbers" do + block = insert(:block) + address = insert(:address) + + log = + insert(:token_transfer_log, + transaction: + insert(:transaction, + block_number: block.number, + block_hash: block.hash, + cumulative_gas_used: 0, + gas_used: 0, + index: 0 + ), + block: block, + address_hash: address.hash + ) + + block_number = log.block_number + assert {:ok, [^block_number]} = TokenTransfer.uncataloged_token_transfer_block_numbers() + end + end end diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index a517ac27d99e..4fb41c3b2407 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -4,10 +4,15 @@ defmodule Explorer.Chain.TransactionTest do import Mox alias Ecto.Changeset - alias Explorer.Chain.Transaction + alias Explorer.Chain.{Address, InternalTransaction, Transaction} + alias Explorer.PagingOptions doctest Transaction + setup :set_mox_global + + setup :verify_on_exit! + describe "changeset/2" do test "with valid attributes" do assert %Changeset{valid?: true} = @@ -265,7 +270,7 @@ defmodule Explorer.Chain.TransactionTest do |> insert() |> Repo.preload(to_address: :smart_contract) - get_eip1967_implementation() + request_zero_implementations() assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 50}]}, _, _} = Transaction.decoded_input_data(transaction, []) @@ -288,7 +293,7 @@ defmodule Explorer.Chain.TransactionTest do |> insert(to_address: contract.address, input: "0x" <> input_data) |> Repo.preload(to_address: :smart_contract) - get_eip1967_implementation() + request_zero_implementations() assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 10}]}, _, _} = Transaction.decoded_input_data(transaction, []) @@ -309,7 +314,482 @@ defmodule Explorer.Chain.TransactionTest do end end - def get_eip1967_implementation do + describe "address_to_transactions_tasks_range_of_blocks/2" do + test "returns empty extremums if no transactions" do + address = insert(:address) + + extremums = Transaction.address_to_transactions_tasks_range_of_blocks(address.hash, []) + + assert extremums == %{ + :min_block_number => nil, + :max_block_number => 0 + } + end + + test "returns correct extremums for from_address" do + address = insert(:address) + + :transaction + |> insert(from_address: address) + |> with_block(insert(:block, number: 1000)) + + extremums = Transaction.address_to_transactions_tasks_range_of_blocks(address.hash, []) + + assert extremums == %{ + :min_block_number => 1000, + :max_block_number => 1000 + } + end + + test "returns correct extremums for to_address" do + address = insert(:address) + + :transaction + |> insert(to_address: address) + |> with_block(insert(:block, number: 1000)) + + extremums = Transaction.address_to_transactions_tasks_range_of_blocks(address.hash, []) + + assert extremums == %{ + :min_block_number => 1000, + :max_block_number => 1000 + } + end + + test "returns correct extremums for created_contract_address" do + address = insert(:address) + + :transaction + |> insert(created_contract_address: address) + |> with_block(insert(:block, number: 1000)) + + extremums = Transaction.address_to_transactions_tasks_range_of_blocks(address.hash, []) + + assert extremums == %{ + :min_block_number => 1000, + :max_block_number => 1000 + } + end + + test "returns correct extremums for multiple number of transactions" do + address = insert(:address) + + :transaction + |> insert(created_contract_address: address) + |> with_block(insert(:block, number: 1000)) + + :transaction + |> insert(created_contract_address: address) + |> with_block(insert(:block, number: 999)) + + :transaction + |> insert(created_contract_address: address) + |> with_block(insert(:block, number: 1003)) + + :transaction + |> insert(from_address: address) + |> with_block(insert(:block, number: 1001)) + + :transaction + |> insert(from_address: address) + |> with_block(insert(:block, number: 1004)) + + :transaction + |> insert(to_address: address) + |> with_block(insert(:block, number: 1002)) + + :transaction + |> insert(to_address: address) + |> with_block(insert(:block, number: 998)) + + extremums = Transaction.address_to_transactions_tasks_range_of_blocks(address.hash, []) + + assert extremums == %{ + :min_block_number => 998, + :max_block_number => 1004 + } + end + end + + describe "address_to_transactions_with_rewards/2" do + test "without transactions" do + %Address{hash: address_hash} = insert(:address) + + assert Repo.aggregate(Transaction, :count, :hash) == 0 + + assert [] == Transaction.address_to_transactions_with_rewards(address_hash) + end + + test "with from transactions" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + assert [transaction] == + Transaction.address_to_transactions_with_rewards(address_hash, direction: :from) + |> Repo.preload([:block, :to_address, :from_address]) + end + + test "with to transactions" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + assert [transaction] == + Transaction.address_to_transactions_with_rewards(address_hash, direction: :to) + |> Repo.preload([:block, :to_address, :from_address]) + end + + test "with to and from transactions and direction: :from" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + # only contains "from" transaction + assert [transaction] == + Transaction.address_to_transactions_with_rewards(address_hash, direction: :from) + |> Repo.preload([:block, :to_address, :from_address]) + end + + test "with to and from transactions and direction: :to" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + assert [transaction] == + Transaction.address_to_transactions_with_rewards(address_hash, direction: :to) + |> Repo.preload([:block, :to_address, :from_address]) + end + + test "with to and from transactions and no :direction option" do + %Address{hash: address_hash} = address = insert(:address) + block = insert(:block) + + transaction1 = + :transaction + |> insert(to_address: address) + |> with_block(block) + + transaction2 = + :transaction + |> insert(from_address: address) + |> with_block(block) + + assert [transaction2, transaction1] == + Transaction.address_to_transactions_with_rewards(address_hash) + |> Repo.preload([:block, :to_address, :from_address]) + end + + test "does not include non-contract-creation parent transactions" do + transaction = + %Transaction{} = + :transaction + |> insert() + |> with_block() + + %InternalTransaction{created_contract_address: address} = + insert(:internal_transaction_create, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + assert [] == Transaction.address_to_transactions_with_rewards(address.hash) + end + + test "returns transactions that have token transfers for the given to_address" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(to_address: address, to_address_hash: address.hash) + |> with_block() + + insert( + :token_transfer, + to_address: address, + transaction: transaction + ) + + assert [transaction.hash] == + Transaction.address_to_transactions_with_rewards(address_hash) + |> Enum.map(& &1.hash) + end + + test "with transactions can be paginated" do + %Address{hash: address_hash} = address = insert(:address) + + second_page_hashes = + 2 + |> insert_list(:transaction, from_address: address) + |> with_block() + |> Enum.map(& &1.hash) + + %Transaction{block_number: block_number, index: index} = + :transaction + |> insert(from_address: address) + |> with_block() + + assert second_page_hashes == + address_hash + |> Transaction.address_to_transactions_with_rewards( + paging_options: %PagingOptions{ + key: {block_number, index}, + page_size: 2 + } + ) + |> Enum.map(& &1.hash) + |> Enum.reverse() + end + + test "returns results in reverse chronological order by block number and transaction index" do + %Address{hash: address_hash} = address = insert(:address) + + a_block = insert(:block, number: 6000) + + %Transaction{hash: first} = + :transaction + |> insert(to_address: address) + |> with_block(a_block) + + %Transaction{hash: second} = + :transaction + |> insert(to_address: address) + |> with_block(a_block) + + %Transaction{hash: third} = + :transaction + |> insert(to_address: address) + |> with_block(a_block) + + %Transaction{hash: fourth} = + :transaction + |> insert(to_address: address) + |> with_block(a_block) + + b_block = insert(:block, number: 2000) + + %Transaction{hash: fifth} = + :transaction + |> insert(to_address: address) + |> with_block(b_block) + + %Transaction{hash: sixth} = + :transaction + |> insert(to_address: address) + |> with_block(b_block) + + result = + address_hash + |> Transaction.address_to_transactions_with_rewards() + |> Enum.map(& &1.hash) + + assert [fourth, third, second, first, sixth, fifth] == result + end + + test "with emission rewards" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + + Application.put_env(:explorer, Explorer.Chain.Block.Reward, + validators_contract_address: "0x0000000000000000000000000000000000000005", + keys_manager_contract_address: "0x0000000000000000000000000000000000000006" + ) + + consumer_pid = start_supervised!(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand) + :erlang.trace(consumer_pid, true, [:receive]) + + block = insert(:block) + + block_miner_hash_string = Base.encode16(block.miner_hash.bytes, case: :lower) + block_miner_hash = block.miner_hash + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :validator + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :emission_funds + ) + + # isValidator => true + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000001"}]} + end + ) + + # getPayoutByMining => 0x0000000000000000000000000000000000000001 + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, [%{id: id, result: "0x000000000000000000000000" <> block_miner_hash_string}]} + end + ) + + res = Transaction.address_to_transactions_with_rewards(block.miner.hash) + assert [{_, _}] = res + + assert_receive {:trace, ^consumer_pid, :receive, {:"$gen_cast", {:fetch_or_update, ^block_miner_hash}}}, 1000 + :timer.sleep(500) + + on_exit(fn -> + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + + Application.put_env(:explorer, Explorer.Chain.Block.Reward, + validators_contract_address: nil, + keys_manager_contract_address: nil + ) + end) + end + + test "with emission rewards and transactions" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + + Application.put_env(:explorer, Explorer.Chain.Block.Reward, + validators_contract_address: "0x0000000000000000000000000000000000000005", + keys_manager_contract_address: "0x0000000000000000000000000000000000000006" + ) + + consumer_pid = start_supervised!(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand) + :erlang.trace(consumer_pid, true, [:receive]) + + block = insert(:block) + + block_miner_hash_string = Base.encode16(block.miner_hash.bytes, case: :lower) + block_miner_hash = block.miner_hash + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :validator + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :emission_funds + ) + + :transaction + |> insert(to_address: block.miner) + |> with_block(block) + |> Repo.preload(:token_transfers) + + # isValidator => true + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000001"}]} + end + ) + + # getPayoutByMining => 0x0000000000000000000000000000000000000001 + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, [%{id: id, result: "0x000000000000000000000000" <> block_miner_hash_string}]} + end + ) + + assert [_, {_, _}] = Transaction.address_to_transactions_with_rewards(block.miner.hash, direction: :to) + + assert_receive {:trace, ^consumer_pid, :receive, {:"$gen_cast", {:fetch_or_update, ^block_miner_hash}}}, 1000 + :timer.sleep(500) + + on_exit(fn -> + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + + Application.put_env(:explorer, Explorer.Chain.Block.Reward, + validators_contract_address: nil, + keys_manager_contract_address: nil + ) + end) + end + + test "with transactions if rewards are not in the range of blocks" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) + + block = insert(:block) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :validator + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :emission_funds + ) + + :transaction + |> insert(from_address: block.miner) + |> with_block() + |> Repo.preload(:token_transfers) + + assert [_] = Transaction.address_to_transactions_with_rewards(block.miner.hash, direction: :from) + + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + end + + test "with emissions rewards, but feature disabled" do + Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) + + block = insert(:block) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :validator + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :emission_funds + ) + + assert [] == Transaction.address_to_transactions_with_rewards(block.miner.hash) + end + end + + # EIP-1967 + EIP-1822 + defp request_zero_implementations do EthereumJSONRPC.Mox |> expect(:json_rpc, fn %{ id: 0, @@ -347,5 +827,63 @@ defmodule Explorer.Chain.TransactionTest do _options -> {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + describe "fee/2" do + test "is_nil(gas_price), is_nil(gas_used)" do + assert {:maximum, nil} == Transaction.fee(%Transaction{gas: 100_500, gas_price: nil, gas_used: nil}, :wei) + end + + test "not is_nil(gas_price), is_nil(gas_used)" do + assert {:maximum, Decimal.new("20100000")} == + Transaction.fee( + %Transaction{gas: 100_500, gas_price: %Explorer.Chain.Wei{value: 200}, gas_used: nil}, + :wei + ) + end + + test "is_nil(gas_price), not is_nil(gas_used)" do + transaction = %Transaction{ + gas_price: nil, + max_priority_fee_per_gas: %Explorer.Chain.Wei{value: 10_000_000_000}, + max_fee_per_gas: %Explorer.Chain.Wei{value: 63_000_000_000}, + gas_used: Decimal.new(100), + block: %{base_fee_per_gas: %Explorer.Chain.Wei{value: 42_000_000_000}} + } + + if Application.get_env(:explorer, :chain_type) == "optimism" do + {:actual, nil} == + Transaction.fee( + transaction, + :wei + ) + else + assert {:actual, Decimal.new("5200000000000")} == + Transaction.fee( + transaction, + :wei + ) + end + end + + test "not is_nil(gas_price), not is_nil(gas_used)" do + assert {:actual, Decimal.new("6")} == + Transaction.fee( + %Transaction{gas_price: %Explorer.Chain.Wei{value: 2}, gas_used: Decimal.new(3)}, + :wei + ) + end end end diff --git a/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs b/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs index a2c66455df52..0bb7be80166a 100644 --- a/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs +++ b/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs @@ -11,6 +11,8 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do setup :set_mox_global + setup :verify_on_exit! + @geth_genesis "#{File.cwd!()}/test/support/fixture/chain_spec/qdai_genesis.json" |> File.read!() |> Jason.decode!() @@ -23,6 +25,10 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do |> File.read!() |> Jason.decode!() + @zkatana_genesis "#{File.cwd!()}/test/support/fixture/chain_spec/zkatana_genesis.json" + |> File.read!() + |> Jason.decode!() + describe "genesis_accounts/1" do test "parses coin balance and contract code" do coin_balances = Importer.genesis_accounts(@geth_genesis) @@ -74,6 +80,23 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do } == List.first(coin_balances) end + + test "parses zkatana coin balance and contract code" do + coin_balances = Importer.genesis_accounts(@zkatana_genesis) + + assert Enum.count(coin_balances) == 9 + + assert %{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<208, 60, 26, 156, 47, 226, 198, 243, 146, 124, 57, 138, 144, 39, 47, 233, 91, 211, 205, 175>> + }, + value: 0, + contract_code: + "0x60806040526004361061006e575f3560e01c8063715018a61161004c578063715018a6146100e25780638da5cb5b146100f6578063e11ae6cb1461011f578063f2fde38b14610132575f80fd5b80632b79805a146100725780634a94d487146100875780636d07dbf81461009a575b5f80fd5b610085610080366004610908565b610151565b005b6100856100953660046109a2565b6101c2565b3480156100a5575f80fd5b506100b96100b43660046109f5565b610203565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b3480156100ed575f80fd5b50610085610215565b348015610101575f80fd5b505f5473ffffffffffffffffffffffffffffffffffffffff166100b9565b61008561012d366004610a15565b610228565b34801561013d575f80fd5b5061008561014c366004610a61565b61028e565b61015961034a565b5f6101658585856103ca565b90506101718183610527565b5060405173ffffffffffffffffffffffffffffffffffffffff821681527fba82f25fed02cd2a23d9f5d11c2ef588d22af5437cbf23bfe61d87257c480e4c9060200160405180910390a15050505050565b6101ca61034a565b6101d583838361056a565b506040517f25adb19089b6a549831a273acdf7908cff8b7ee5f551f8d1d37996cf01c5df5b905f90a1505050565b5f61020e8383610598565b9392505050565b61021d61034a565b6102265f6105a4565b565b61023061034a565b5f61023c8484846103ca565b60405173ffffffffffffffffffffffffffffffffffffffff821681529091507fba82f25fed02cd2a23d9f5d11c2ef588d22af5437cbf23bfe61d87257c480e4c9060200160405180910390a150505050565b61029661034a565b73ffffffffffffffffffffffffffffffffffffffff811661033e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b610347816105a4565b50565b5f5473ffffffffffffffffffffffffffffffffffffffff163314610226576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610335565b5f83471015610435576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f437265617465323a20696e73756666696369656e742062616c616e63650000006044820152606401610335565b81515f0361049f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f437265617465323a2062797465636f6465206c656e677468206973207a65726f6044820152606401610335565b8282516020840186f5905073ffffffffffffffffffffffffffffffffffffffff811661020e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601960248201527f437265617465323a204661696c6564206f6e206465706c6f79000000000000006044820152606401610335565b606061020e83835f6040518060400160405280601e81526020017f416464726573733a206c6f772d6c6576656c2063616c6c206661696c65640000815250610618565b6060610590848484604051806060016040528060298152602001610b0860299139610618565b949350505050565b5f61020e83833061072d565b5f805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6060824710156106aa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f60448201527f722063616c6c00000000000000000000000000000000000000000000000000006064820152608401610335565b5f808673ffffffffffffffffffffffffffffffffffffffff1685876040516106d29190610a9c565b5f6040518083038185875af1925050503d805f811461070c576040519150601f19603f3d011682016040523d82523d5f602084013e610711565b606091505b509150915061072287838387610756565b979650505050505050565b5f604051836040820152846020820152828152600b8101905060ff815360559020949350505050565b606083156107eb5782515f036107e45773ffffffffffffffffffffffffffffffffffffffff85163b6107e4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610335565b5081610590565b61059083838151156108005781518083602001fd5b806040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103359190610ab7565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f82601f830112610870575f80fd5b813567ffffffffffffffff8082111561088b5761088b610834565b604051601f83017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f011681019082821181831017156108d1576108d1610834565b816040528381528660208588010111156108e9575f80fd5b836020870160208301375f602085830101528094505050505092915050565b5f805f806080858703121561091b575f80fd5b8435935060208501359250604085013567ffffffffffffffff80821115610940575f80fd5b61094c88838901610861565b93506060870135915080821115610961575f80fd5b5061096e87828801610861565b91505092959194509250565b803573ffffffffffffffffffffffffffffffffffffffff8116811461099d575f80fd5b919050565b5f805f606084860312156109b4575f80fd5b6109bd8461097a565b9250602084013567ffffffffffffffff8111156109d8575f80fd5b6109e486828701610861565b925050604084013590509250925092565b5f8060408385031215610a06575f80fd5b50508035926020909101359150565b5f805f60608486031215610a27575f80fd5b8335925060208401359150604084013567ffffffffffffffff811115610a4b575f80fd5b610a5786828701610861565b9150509250925092565b5f60208284031215610a71575f80fd5b61020e8261097a565b5f5b83811015610a94578181015183820152602001610a7c565b50505f910152565b5f8251610aad818460208701610a7a565b9190910192915050565b602081525f8251806020840152610ad5816040850160208701610a7a565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2063616c6c20776974682076616c7565206661696c6564a2646970667358221220330b94dc698c4d290bf55c23f13b473cde6a6bae0030cb902de18af54e35839f64736f6c63430008140033" + } == + List.first(coin_balances) + end end describe "import_genesis_accounts/1" do diff --git a/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs b/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs index 3f74ea550b67..e610fd661dcd 100644 --- a/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs +++ b/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs @@ -12,6 +12,8 @@ defmodule Explorer.ChainSpec.Parity.ImporterTest do setup :set_mox_global + setup :verify_on_exit! + @chain_spec "#{File.cwd!()}/test/support/fixture/chain_spec/foundation.json" |> File.read!() |> Jason.decode!() diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index d5756d0688b8..36d4647adb7d 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -23,7 +23,6 @@ defmodule Explorer.ChainTest do Token, TokenTransfer, Transaction, - SmartContract, Wei } @@ -38,6 +37,10 @@ defmodule Explorer.ChainTest do alias Explorer.Counters.AddressesWithBalanceCounter alias Explorer.Counters.AddressesCounter + @first_topic_hex_string "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @second_topic_hex_string "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + @third_topic_hex_string "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d" + doctest Explorer.Chain setup :set_mox_global @@ -145,8 +148,8 @@ defmodule Explorer.ChainTest do end end - describe "ERC721_or_ERC1155_token_instance_from_token_id_and_token_address/2" do - test "return ERC721 token instance" do + describe "nft_instance_from_token_id_and_token_address/2" do + test "return NFT instance" do token = insert(:token) token_id = 10 @@ -157,7 +160,7 @@ defmodule Explorer.ChainTest do ) assert {:ok, result} = - Chain.erc721_or_erc1155_token_instance_from_token_id_and_token_address( + Chain.nft_instance_from_token_id_and_token_address( token_id, token.contract_address_hash ) @@ -316,6 +319,9 @@ defmodule Explorer.ChainTest do block_number: transaction1.block_number ) + first_topic_hex_string = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(first_topic_hex_string) + transaction2 = :transaction |> insert(from_address: address) @@ -326,11 +332,11 @@ defmodule Explorer.ChainTest do transaction: transaction2, index: 2, address: address, - first_topic: "test", + first_topic: first_topic, block_number: transaction2.block_number ) - [found_log] = Chain.address_to_logs(address_hash, false, topic: "test") + [found_log] = Chain.address_to_logs(address_hash, false, topic: first_topic_hex_string) assert found_log.transaction.hash == transaction2.hash end @@ -343,505 +349,34 @@ defmodule Explorer.ChainTest do |> insert(to_address: address) |> with_block() - insert(:log, - block: transaction1.block, - block_number: transaction1.block_number, - transaction: transaction1, - index: 1, - address: address, - fourth_topic: "test" - ) - - transaction2 = - :transaction - |> insert(from_address: address) - |> with_block() - - insert(:log, - block: transaction2.block, - block_number: transaction2.block.number, - transaction: transaction2, - index: 2, - address: address - ) - - [found_log] = Chain.address_to_logs(address_hash, false, topic: "test") - - assert found_log.transaction.hash == transaction1.hash - end - end - - describe "address_to_transactions_with_rewards/2" do - test "without transactions" do - %Address{hash: address_hash} = insert(:address) - - assert Repo.aggregate(Transaction, :count, :hash) == 0 - - assert [] == Chain.address_to_transactions_with_rewards(address_hash) - end - - test "with from transactions" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(from_address: address) - |> with_block() - - assert [transaction] == - Chain.address_to_transactions_with_rewards(address_hash, direction: :from) - |> Repo.preload([:block, :to_address, :from_address]) - end - - test "with to transactions" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(to_address: address) - |> with_block() - - assert [transaction] == - Chain.address_to_transactions_with_rewards(address_hash, direction: :to) - |> Repo.preload([:block, :to_address, :from_address]) - end - - test "with to and from transactions and direction: :from" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(from_address: address) - |> with_block() - - # only contains "from" transaction - assert [transaction] == - Chain.address_to_transactions_with_rewards(address_hash, direction: :from) - |> Repo.preload([:block, :to_address, :from_address]) - end - - test "with to and from transactions and direction: :to" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(to_address: address) - |> with_block() - - assert [transaction] == - Chain.address_to_transactions_with_rewards(address_hash, direction: :to) - |> Repo.preload([:block, :to_address, :from_address]) - end - - test "with to and from transactions and no :direction option" do - %Address{hash: address_hash} = address = insert(:address) - block = insert(:block) - - transaction1 = - :transaction - |> insert(to_address: address) - |> with_block(block) - - transaction2 = - :transaction - |> insert(from_address: address) - |> with_block(block) - - assert [transaction2, transaction1] == - Chain.address_to_transactions_with_rewards(address_hash) - |> Repo.preload([:block, :to_address, :from_address]) - end - - test "does not include non-contract-creation parent transactions" do - transaction = - %Transaction{} = - :transaction - |> insert() - |> with_block() - - %InternalTransaction{created_contract_address: address} = - insert(:internal_transaction_create, - transaction: transaction, - index: 0, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - assert [] == Chain.address_to_transactions_with_rewards(address.hash) - end - - test "returns transactions that have token transfers for the given to_address" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(to_address: address, to_address_hash: address.hash) - |> with_block() - - insert( - :token_transfer, - to_address: address, - transaction: transaction - ) - - assert [transaction.hash] == - Chain.address_to_transactions_with_rewards(address_hash) - |> Enum.map(& &1.hash) - end - - test "with transactions can be paginated" do - %Address{hash: address_hash} = address = insert(:address) - - second_page_hashes = - 2 - |> insert_list(:transaction, from_address: address) - |> with_block() - |> Enum.map(& &1.hash) - - %Transaction{block_number: block_number, index: index} = - :transaction - |> insert(from_address: address) - |> with_block() - - assert second_page_hashes == - address_hash - |> Chain.address_to_transactions_with_rewards( - paging_options: %PagingOptions{ - key: {block_number, index}, - page_size: 2 - } - ) - |> Enum.map(& &1.hash) - |> Enum.reverse() - end - - test "returns results in reverse chronological order by block number and transaction index" do - %Address{hash: address_hash} = address = insert(:address) - - a_block = insert(:block, number: 6000) - - %Transaction{hash: first} = - :transaction - |> insert(to_address: address) - |> with_block(a_block) - - %Transaction{hash: second} = - :transaction - |> insert(to_address: address) - |> with_block(a_block) - - %Transaction{hash: third} = - :transaction - |> insert(to_address: address) - |> with_block(a_block) - - %Transaction{hash: fourth} = - :transaction - |> insert(to_address: address) - |> with_block(a_block) - - b_block = insert(:block, number: 2000) - - %Transaction{hash: fifth} = - :transaction - |> insert(to_address: address) - |> with_block(b_block) - - %Transaction{hash: sixth} = - :transaction - |> insert(to_address: address) - |> with_block(b_block) - - result = - address_hash - |> Chain.address_to_transactions_with_rewards() - |> Enum.map(& &1.hash) - - assert [fourth, third, second, first, sixth, fifth] == result - end - - test "with emission rewards" do - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) - - Application.put_env(:explorer, Explorer.Chain.Block.Reward, - validators_contract_address: "0x0000000000000000000000000000000000000005", - keys_manager_contract_address: "0x0000000000000000000000000000000000000006" - ) - - consumer_pid = start_supervised!(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand) - :erlang.trace(consumer_pid, true, [:receive]) - - block = insert(:block) - - block_miner_hash_string = Base.encode16(block.miner_hash.bytes, case: :lower) - block_miner_hash = block.miner_hash - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :validator - ) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :emission_funds - ) - - # isValidator => true - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000001"}]} - end - ) - - # getPayoutByMining => 0x0000000000000000000000000000000000000001 - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, [%{id: id, result: "0x000000000000000000000000" <> block_miner_hash_string}]} - end - ) - - res = Chain.address_to_transactions_with_rewards(block.miner.hash) - assert [{_, _}] = res - - assert_receive {:trace, ^consumer_pid, :receive, {:"$gen_cast", {:fetch_or_update, ^block_miner_hash}}}, 1000 - :timer.sleep(500) - - on_exit(fn -> - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) - - Application.put_env(:explorer, Explorer.Chain.Block.Reward, - validators_contract_address: nil, - keys_manager_contract_address: nil - ) - end) - end - - test "with emission rewards and transactions" do - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) - - Application.put_env(:explorer, Explorer.Chain.Block.Reward, - validators_contract_address: "0x0000000000000000000000000000000000000005", - keys_manager_contract_address: "0x0000000000000000000000000000000000000006" - ) - - consumer_pid = start_supervised!(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand) - :erlang.trace(consumer_pid, true, [:receive]) - - block = insert(:block) - - block_miner_hash_string = Base.encode16(block.miner_hash.bytes, case: :lower) - block_miner_hash = block.miner_hash - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :validator - ) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :emission_funds - ) - - :transaction - |> insert(to_address: block.miner) - |> with_block(block) - |> Repo.preload(:token_transfers) - - # isValidator => true - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000000000000000000000000000000000000000000001"}]} - end - ) - - # getPayoutByMining => 0x0000000000000000000000000000000000000001 - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, [%{id: id, result: "0x000000000000000000000000" <> block_miner_hash_string}]} - end - ) - - assert [_, {_, _}] = Chain.address_to_transactions_with_rewards(block.miner.hash, direction: :to) - - assert_receive {:trace, ^consumer_pid, :receive, {:"$gen_cast", {:fetch_or_update, ^block_miner_hash}}}, 1000 - :timer.sleep(500) - - on_exit(fn -> - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) - - Application.put_env(:explorer, Explorer.Chain.Block.Reward, - validators_contract_address: nil, - keys_manager_contract_address: nil - ) - end) - end - - test "with transactions if rewards are not in the range of blocks" do - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: true) - - block = insert(:block) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :validator - ) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :emission_funds - ) - - :transaction - |> insert(from_address: block.miner) - |> with_block() - |> Repo.preload(:token_transfers) - - assert [_] = Chain.address_to_transactions_with_rewards(block.miner.hash, direction: :from) - - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) - end - - test "with emissions rewards, but feature disabled" do - Application.put_env(:block_scout_web, BlockScoutWeb.Chain, has_emission_funds: false) - - block = insert(:block) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :validator - ) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :emission_funds - ) - - assert [] == Chain.address_to_transactions_with_rewards(block.miner.hash) - end - end - - describe "address_to_transactions_tasks_range_of_blocks/2" do - test "returns empty extremums if no transactions" do - address = insert(:address) - - extremums = Chain.address_to_transactions_tasks_range_of_blocks(address.hash, []) - - assert extremums == %{ - :min_block_number => nil, - :max_block_number => 0 - } - end - - test "returns correct extremums for from_address" do - address = insert(:address) - - :transaction - |> insert(from_address: address) - |> with_block(insert(:block, number: 1000)) - - extremums = Chain.address_to_transactions_tasks_range_of_blocks(address.hash, []) - - assert extremums == %{ - :min_block_number => 1000, - :max_block_number => 1000 - } - end - - test "returns correct extremums for to_address" do - address = insert(:address) - - :transaction - |> insert(to_address: address) - |> with_block(insert(:block, number: 1000)) - - extremums = Chain.address_to_transactions_tasks_range_of_blocks(address.hash, []) - - assert extremums == %{ - :min_block_number => 1000, - :max_block_number => 1000 - } - end - - test "returns correct extremums for created_contract_address" do - address = insert(:address) - - :transaction - |> insert(created_contract_address: address) - |> with_block(insert(:block, number: 1000)) - - extremums = Chain.address_to_transactions_tasks_range_of_blocks(address.hash, []) - - assert extremums == %{ - :min_block_number => 1000, - :max_block_number => 1000 - } - end - - test "returns correct extremums for multiple number of transactions" do - address = insert(:address) - - :transaction - |> insert(created_contract_address: address) - |> with_block(insert(:block, number: 1000)) - - :transaction - |> insert(created_contract_address: address) - |> with_block(insert(:block, number: 999)) - - :transaction - |> insert(created_contract_address: address) - |> with_block(insert(:block, number: 1003)) - - :transaction - |> insert(from_address: address) - |> with_block(insert(:block, number: 1001)) + fourth_topic_hex_string = "0x927abf391899d10d331079a63caffa905efa7075a44a7bbd52b190db4c4308fb" + {:ok, fourth_topic} = Explorer.Chain.Hash.Full.cast(fourth_topic_hex_string) - :transaction - |> insert(from_address: address) - |> with_block(insert(:block, number: 1004)) + insert(:log, + block: transaction1.block, + block_number: transaction1.block_number, + transaction: transaction1, + index: 1, + address: address, + fourth_topic: fourth_topic + ) - :transaction - |> insert(to_address: address) - |> with_block(insert(:block, number: 1002)) + transaction2 = + :transaction + |> insert(from_address: address) + |> with_block() - :transaction - |> insert(to_address: address) - |> with_block(insert(:block, number: 998)) + insert(:log, + block: transaction2.block, + block_number: transaction2.block.number, + transaction: transaction2, + index: 2, + address: address + ) - extremums = Chain.address_to_transactions_tasks_range_of_blocks(address.hash, []) + [found_log] = Chain.address_to_logs(address_hash, false, topic: fourth_topic_hex_string) - assert extremums == %{ - :min_block_number => 998, - :max_block_number => 1004 - } + assert found_log.transaction.hash == transaction1.hash end end @@ -1108,22 +643,31 @@ defmodule Explorer.ChainTest do describe "fee/2" do test "without receipt with :wei unit" do - assert Chain.fee(%Transaction{gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, gas_used: nil}, :wei) == + assert Transaction.fee( + %Transaction{gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, gas_used: nil}, + :wei + ) == {:maximum, Decimal.new(6)} end test "without receipt with :gwei unit" do - assert Chain.fee(%Transaction{gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, gas_used: nil}, :gwei) == + assert Transaction.fee( + %Transaction{gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, gas_used: nil}, + :gwei + ) == {:maximum, Decimal.new("6e-9")} end test "without receipt with :ether unit" do - assert Chain.fee(%Transaction{gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, gas_used: nil}, :ether) == + assert Transaction.fee( + %Transaction{gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, gas_used: nil}, + :ether + ) == {:maximum, Decimal.new("6e-18")} end test "with receipt with :wei unit" do - assert Chain.fee( + assert Transaction.fee( %Transaction{ gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, @@ -1134,7 +678,7 @@ defmodule Explorer.ChainTest do end test "with receipt with :gwei unit" do - assert Chain.fee( + assert Transaction.fee( %Transaction{ gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, @@ -1145,7 +689,7 @@ defmodule Explorer.ChainTest do end test "with receipt with :ether unit" do - assert Chain.fee( + assert Transaction.fee( %Transaction{ gas: Decimal.new(3), gas_price: %Wei{value: Decimal.new(2)}, @@ -1352,7 +896,7 @@ defmodule Explorer.ChainTest do address = insert(:address) insert(:decompiled_smart_contract, address_hash: address.hash) - {:ok, found_address} = Chain.hash_to_address(address.hash) + {:ok, found_address} = Chain.hash_to_address(address.hash, [], true) assert found_address.has_decompiled_code? end @@ -1492,8 +1036,8 @@ defmodule Explorer.ChainTest do assert Decimal.compare(Chain.indexed_ratio_blocks(), Decimal.from_float(0.5)) == :eq end - test "returns 0 if no blocks" do - assert Decimal.new(0) == Chain.indexed_ratio_blocks() + test "returns 1 if no blocks" do + assert Decimal.new(1) == Chain.indexed_ratio_blocks() end test "returns 1.0 if fully indexed blocks" do @@ -1712,6 +1256,10 @@ defmodule Explorer.ChainTest do # Full tests in `test/explorer/import_test.exs` describe "import/1" do + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string) + {:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string) + {:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string) + @import_data %{ blocks: %{ params: [ @@ -1746,7 +1294,8 @@ defmodule Explorer.ChainTest do %{ block_number: 37, transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - index: 0, + # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth + index: 1, trace_address: [], type: "call", call_type: "call", @@ -1767,13 +1316,12 @@ defmodule Explorer.ChainTest do block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: first_topic, + second_topic: second_topic, + third_topic: third_topic, fourth_topic: nil, index: 0, - transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", - type: "mined" + transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } ] }, @@ -1829,19 +1377,26 @@ defmodule Explorer.ChainTest do from_address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", to_address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + token_type: "ERC-20", transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" } ] } } - test "with valid data" do + test "with valid data", %{json_rpc_named_arguments: _json_rpc_named_arguments} do + {:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string) + {:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string) + {:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string) difficulty = Decimal.new(340_282_366_920_938_463_463_374_607_431_768_211_454) total_difficulty = Decimal.new(12_590_447_576_074_723_148_144_860_474_975_121_280_509) token_transfer_amount = Decimal.new(1_000_000_000_000_000_000) gas_limit = Decimal.new(6_946_336) gas_used = Decimal.new(50450) + gas_int = Decimal.new("4677320") + gas_used_int = Decimal.new("27770") + assert {:ok, %{ addresses: [ @@ -1923,7 +1478,41 @@ defmodule Explorer.ChainTest do updated_at: %{} } ], - internal_transactions: [], + internal_transactions: [ + %InternalTransaction{ + call_type: :call, + created_contract_code: nil, + error: nil, + gas: ^gas_int, + gas_used: ^gas_used_int, + index: 1, + init: nil, + input: %Explorer.Chain.Data{ + bytes: + <<16, 133, 82, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7, 115, 238, 63, 140, + 231, 234, 137, 179, 40, 255, 234, 134, 26, 179, 239>> + }, + output: %Explorer.Chain.Data{bytes: ""}, + trace_address: [], + type: :call, + block_number: 37, + transaction_index: nil, + block_index: 0, + created_contract_address_hash: nil, + from_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<232, 221, 197, 199, 162, 210, 240, 215, 169, 121, 132, 89, 192, 16, 79, 223, 94, 152, 122, + 202>> + }, + to_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, + 91>> + } + } + ], logs: [ %Log{ address_hash: %Hash{ @@ -1938,9 +1527,9 @@ defmodule Explorer.ChainTest do 167, 100, 0, 0>> }, index: 0, - first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - second_topic: "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", - third_topic: "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + first_topic: ^first_topic, + second_topic: ^second_topic, + third_topic: ^third_topic, fourth_topic: nil, transaction_hash: %Hash{ byte_count: 32, @@ -1948,7 +1537,6 @@ defmodule Explorer.ChainTest do <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> }, - type: "mined", inserted_at: %{}, updated_at: %{} } @@ -2058,76 +1646,6 @@ defmodule Explorer.ChainTest do end end - describe "list_top_addresses/0" do - test "without addresses with balance > 0" do - insert(:address, fetched_coin_balance: 0) - assert [] = Chain.list_top_addresses() - end - - test "with top addresses in order" do - address_hashes = - 4..1 - |> Enum.map(&insert(:address, fetched_coin_balance: &1)) - |> Enum.map(& &1.hash) - - assert address_hashes == - Chain.list_top_addresses() - |> Enum.map(fn {address, _transaction_count} -> address end) - |> Enum.map(& &1.hash) - end - - # flaky test - # test "with top addresses in order with matching value" do - # test_hashes = - # 4..0 - # |> Enum.map(&Explorer.Chain.Hash.cast(Explorer.Chain.Hash.Address, &1)) - # |> Enum.map(&elem(&1, 1)) - - # tail = - # 4..1 - # |> Enum.map(&insert(:address, fetched_coin_balance: &1, hash: Enum.fetch!(test_hashes, &1 - 1))) - # |> Enum.map(& &1.hash) - - # first_result_hash = - # :address - # |> insert(fetched_coin_balance: 4, hash: Enum.fetch!(test_hashes, 4)) - # |> Map.fetch!(:hash) - - # assert [first_result_hash | tail] == - # Chain.list_top_addresses() - # |> Enum.map(fn {address, _transaction_count} -> address end) - # |> Enum.map(& &1.hash) - # end - - # flaky test - # test "paginates addresses" do - # test_hashes = - # 4..0 - # |> Enum.map(&Explorer.Chain.Hash.cast(Explorer.Chain.Hash.Address, &1)) - # |> Enum.map(&elem(&1, 1)) - - # result = - # 4..1 - # |> Enum.map(&insert(:address, fetched_coin_balance: &1, hash: Enum.fetch!(test_hashes, &1 - 1))) - # |> Enum.map(& &1.hash) - - # options = [paging_options: %PagingOptions{page_size: 1}] - - # [{top_address, _}] = Chain.list_top_addresses(options) - # assert top_address.hash == List.first(result) - - # tail_options = [ - # paging_options: %PagingOptions{key: {top_address.fetched_coin_balance.value, top_address.hash}, page_size: 3} - # ] - - # tail_result = tail_options |> Chain.list_top_addresses() |> Enum.map(fn {address, _} -> address.hash end) - - # [_ | expected_tail] = result - - # assert tail_result == expected_tail - # end - end - describe "stream_blocks_without_rewards/2" do test "includes consensus blocks" do %Block{hash: consensus_hash} = insert(:block, consensus: true) @@ -3613,7 +3131,7 @@ defmodule Explorer.ChainTest do :contracts_creation_internal_transaction, :contracts_creation_transaction, :token, - :smart_contract_additional_sources + [smart_contract: :smart_contract_additional_sources] ]) options = [ @@ -3644,100 +3162,29 @@ defmodule Explorer.ChainTest do end end - describe "block_reward/1" do - setup do - %{block_range: range} = emission_reward = insert(:emission_reward) - - block = insert(:block, number: Enum.random(Range.new(range.from, range.to))) - insert(:transaction) - - {:ok, block: block, emission_reward: emission_reward} - end - - test "with block containing transactions", %{block: block, emission_reward: emission_reward} do - :transaction - |> insert(gas_price: 1) - |> with_block(block, gas_used: 1) - - :transaction - |> insert(gas_price: 1) - |> with_block(block, gas_used: 2) - - expected = - emission_reward.reward - |> Wei.to(:wei) - |> Decimal.add(Decimal.new(3)) - |> Wei.from(:wei) - - assert expected == Chain.block_reward(block.number) - end - - test "with block without transactions", %{block: block, emission_reward: emission_reward} do - assert emission_reward.reward == Chain.block_reward(block.number) - end - end - - describe "block_reward_by_parts/1" do - setup do - {:ok, emission_reward: insert(:emission_reward)} - end - - test "without uncles", %{emission_reward: %{reward: reward, block_range: range}} do - block = build(:block, number: range.from, base_fee_per_gas: 5, uncles: []) - - tx1 = build(:transaction, gas_price: 1, gas_used: 1, block_number: block.number, block_hash: block.hash) - tx2 = build(:transaction, gas_price: 1, gas_used: 2, block_number: block.number, block_hash: block.hash) - - tx3 = - build(:transaction, - gas_price: 1, - gas_used: 3, - block_number: block.number, - block_hash: block.hash, - max_priority_fee_per_gas: 1 - ) - - expected_txn_fees = %Wei{value: Decimal.new(6)} - expected_burned_fees = %Wei{value: Decimal.new(30)} - expected_uncle_reward = %Wei{value: Decimal.new(0)} - - assert %{ - static_reward: ^reward, - txn_fees: ^expected_txn_fees, - burned_fees: ^expected_burned_fees, - uncle_reward: ^expected_uncle_reward - } = Chain.block_reward_by_parts(block, [tx1, tx2, tx3]) - end - - test "with uncles", %{emission_reward: %{reward: reward, block_range: range}} do - block = - build(:block, number: range.from, uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]) - - expected_uncle_reward = Wei.mult(reward, Decimal.from_float(1 / 32)) - - assert %{uncle_reward: ^expected_uncle_reward} = Chain.block_reward_by_parts(block, []) - end - end - describe "gas_payment_by_block_hash/1" do setup do number = 1 - %{consensus_block: insert(:block, number: number, consensus: true), number: number} + block = insert(:block, number: number, consensus: true) + + %{consensus_block: block, number: number} end - test "without consensus block hash has no key", %{consensus_block: consensus_block, number: number} do + test "without consensus block hash has key with 0 value", %{consensus_block: consensus_block, number: number} do non_consensus_block = insert(:block, number: number, consensus: false) :transaction - |> insert(gas_price: 1) + |> insert(gas_price: 1, block_consensus: false) |> with_block(consensus_block, gas_used: 1) :transaction - |> insert(gas_price: 1) + |> insert(gas_price: 1, block_consensus: false) |> with_block(consensus_block, gas_used: 2) - assert Chain.gas_payment_by_block_hash([non_consensus_block.hash]) == %{} + assert Chain.gas_payment_by_block_hash([non_consensus_block.hash]) == %{ + non_consensus_block.hash => %Wei{value: Decimal.new(0)} + } end test "with consensus block hash without transactions has key with 0 value", %{ @@ -3989,316 +3436,50 @@ defmodule Explorer.ChainTest do test "with invalid params can't create decompiled smart contract" do params = %{code: "cat"} - {:error, _changeset} = Chain.create_decompiled_smart_contract(params) - end - - test "updates smart contract code" do - inserted_decompiled_smart_contract = insert(:decompiled_smart_contract) - code = "code2" - - {:ok, _decompiled_smart_contract} = - Chain.create_decompiled_smart_contract(%{ - decompiler_version: inserted_decompiled_smart_contract.decompiler_version, - decompiled_source_code: code, - address_hash: inserted_decompiled_smart_contract.address_hash - }) - - decompiled_smart_contract = - Repo.one( - from(ds in DecompiledSmartContract, - where: - ds.address_hash == ^inserted_decompiled_smart_contract.address_hash and - ds.decompiler_version == ^inserted_decompiled_smart_contract.decompiler_version - ) - ) - - assert decompiled_smart_contract.decompiled_source_code == code - end - - test "creates two smart contracts for different decompiler versions" do - inserted_decompiled_smart_contract = insert(:decompiled_smart_contract) - code = "code2" - version = "2" - - {:ok, _decompiled_smart_contract} = - Chain.create_decompiled_smart_contract(%{ - decompiler_version: version, - decompiled_source_code: code, - address_hash: inserted_decompiled_smart_contract.address_hash - }) - - decompiled_smart_contracts = - Repo.all( - from(ds in DecompiledSmartContract, where: ds.address_hash == ^inserted_decompiled_smart_contract.address_hash) - ) - - assert Enum.count(decompiled_smart_contracts) == 2 - end - end - - describe "create_smart_contract/1" do - setup do - smart_contract_bytecode = - "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" - - created_contract_address = - insert( - :address, - hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", - contract_code: smart_contract_bytecode - ) - - transaction = - :transaction - |> insert() - |> with_block() - - insert( - :internal_transaction_create, - transaction: transaction, - index: 0, - created_contract_address: created_contract_address, - created_contract_code: smart_contract_bytecode, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - valid_attrs = %{ - address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", - name: "SimpleStorage", - compiler_version: "0.4.23", - optimization: false, - contract_source_code: - "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", - abi: [ - %{ - "constant" => false, - "inputs" => [%{"name" => "x", "type" => "uint256"}], - "name" => "set", - "outputs" => [], - "payable" => false, - "stateMutability" => "nonpayable", - "type" => "function" - }, - %{ - "constant" => true, - "inputs" => [], - "name" => "get", - "outputs" => [%{"name" => "", "type" => "uint256"}], - "payable" => false, - "stateMutability" => "view", - "type" => "function" - } - ] - } - - {:ok, valid_attrs: valid_attrs, address: created_contract_address} - end - - test "with valid data creates a smart contract", %{valid_attrs: valid_attrs} do - assert {:ok, %SmartContract{} = smart_contract} = Chain.create_smart_contract(valid_attrs) - assert smart_contract.name == "SimpleStorage" - assert smart_contract.compiler_version == "0.4.23" - assert smart_contract.optimization == false - assert smart_contract.contract_source_code != "" - assert smart_contract.abi != "" - - assert Repo.get_by( - Address.Name, - address_hash: smart_contract.address_hash, - name: smart_contract.name, - primary: true - ) - end - - test "clears an existing primary name and sets the new one", %{valid_attrs: valid_attrs, address: address} do - insert(:address_name, address: address, primary: true) - assert {:ok, %SmartContract{} = smart_contract} = Chain.create_smart_contract(valid_attrs) - - assert Repo.get_by( - Address.Name, - address_hash: smart_contract.address_hash, - name: smart_contract.name, - primary: true - ) - end - - test "trims whitespace from address name", %{valid_attrs: valid_attrs} do - attrs = %{valid_attrs | name: " SimpleStorage "} - assert {:ok, _} = Chain.create_smart_contract(attrs) - assert Repo.get_by(Address.Name, name: "SimpleStorage") - end - - test "sets the address verified field to true", %{valid_attrs: valid_attrs} do - assert {:ok, %SmartContract{} = smart_contract} = Chain.create_smart_contract(valid_attrs) - - assert Repo.get_by(Address, hash: smart_contract.address_hash).verified == true - end - end - - describe "update_smart_contract/1" do - setup do - smart_contract_bytecode = - "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" - - created_contract_address = - insert( - :address, - hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", - contract_code: smart_contract_bytecode - ) - - transaction = - :transaction - |> insert() - |> with_block() - - insert( - :internal_transaction_create, - transaction: transaction, - index: 0, - created_contract_address: created_contract_address, - created_contract_code: smart_contract_bytecode, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - valid_attrs = %{ - address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", - name: "SimpleStorage", - compiler_version: "0.4.23", - optimization: false, - contract_source_code: - "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", - abi: [ - %{ - "constant" => false, - "inputs" => [%{"name" => "x", "type" => "uint256"}], - "name" => "set", - "outputs" => [], - "payable" => false, - "stateMutability" => "nonpayable", - "type" => "function" - }, - %{ - "constant" => true, - "inputs" => [], - "name" => "get", - "outputs" => [%{"name" => "", "type" => "uint256"}], - "payable" => false, - "stateMutability" => "view", - "type" => "function" - } - ], - partially_verified: true - } - - secondary_sources = [ - %{ - file_name: "storage.sol", - contract_source_code: - "pragma solidity >=0.7.0 <0.9.0;contract Storage {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", - address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" - }, - %{ - file_name: "storage_1.sol", - contract_source_code: - "pragma solidity >=0.7.0 <0.9.0;contract Storage_1 {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", - address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" - } - ] - - changed_sources = [ - %{ - file_name: "storage_2.sol", - contract_source_code: - "pragma solidity >=0.7.0 <0.9.0;contract Storage_2 {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", - address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" - }, - %{ - file_name: "storage_3.sol", - contract_source_code: - "pragma solidity >=0.7.0 <0.9.0;contract Storage_3 {uint256 number;function store(uint256 num) public {number = num;}function retrieve_() public view returns (uint256){return number;}}", - address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c" - } - ] - - _ = Chain.create_smart_contract(valid_attrs, [], secondary_sources) - - {:ok, - valid_attrs: valid_attrs, - address: created_contract_address, - secondary_sources: secondary_sources, - changed_sources: changed_sources} - end - - test "change partially_verified field", %{valid_attrs: valid_attrs, address: address} do - sc_before_call = Repo.get_by(SmartContract, address_hash: address.hash) - assert sc_before_call.name == Map.get(valid_attrs, :name) - assert sc_before_call.partially_verified == Map.get(valid_attrs, :partially_verified) - - assert {:ok, %SmartContract{}} = - Chain.update_smart_contract(%{ - address_hash: address.hash, - partially_verified: false, - contract_source_code: "new code" - }) - - sc_after_call = Repo.get_by(SmartContract, address_hash: address.hash) - assert sc_after_call.name == Map.get(valid_attrs, :name) - assert sc_after_call.partially_verified == false - assert sc_after_call.compiler_version == Map.get(valid_attrs, :compiler_version) - assert sc_after_call.optimization == Map.get(valid_attrs, :optimization) - assert sc_after_call.contract_source_code == "new code" - end - - test "check nothing changed", %{valid_attrs: valid_attrs, address: address} do - sc_before_call = Repo.get_by(SmartContract, address_hash: address.hash) - assert sc_before_call.name == Map.get(valid_attrs, :name) - assert sc_before_call.partially_verified == Map.get(valid_attrs, :partially_verified) - - assert {:ok, %SmartContract{}} = Chain.update_smart_contract(%{address_hash: address.hash}) - - sc_after_call = Repo.get_by(SmartContract, address_hash: address.hash) - assert sc_after_call.name == Map.get(valid_attrs, :name) - assert sc_after_call.partially_verified == Map.get(valid_attrs, :partially_verified) - assert sc_after_call.compiler_version == Map.get(valid_attrs, :compiler_version) - assert sc_after_call.optimization == Map.get(valid_attrs, :optimization) - assert sc_after_call.contract_source_code == Map.get(valid_attrs, :contract_source_code) + {:error, _changeset} = Chain.create_decompiled_smart_contract(params) end - test "check additional sources update", %{ - address: address, - secondary_sources: secondary_sources, - changed_sources: changed_sources - } do - sc_before_call = Repo.get_by(Address, hash: address.hash) |> Repo.preload(:smart_contract_additional_sources) + test "updates smart contract code" do + inserted_decompiled_smart_contract = insert(:decompiled_smart_contract) + code = "code2" - assert sc_before_call.smart_contract_additional_sources - |> Enum.with_index() - |> Enum.all?(fn {el, ind} -> - {:ok, src} = Enum.fetch(secondary_sources, ind) + {:ok, _decompiled_smart_contract} = + Chain.create_decompiled_smart_contract(%{ + decompiler_version: inserted_decompiled_smart_contract.decompiler_version, + decompiled_source_code: code, + address_hash: inserted_decompiled_smart_contract.address_hash + }) - el.file_name == Map.get(src, :file_name) and - el.contract_source_code == Map.get(src, :contract_source_code) - end) + decompiled_smart_contract = + Repo.one( + from(ds in DecompiledSmartContract, + where: + ds.address_hash == ^inserted_decompiled_smart_contract.address_hash and + ds.decompiler_version == ^inserted_decompiled_smart_contract.decompiler_version + ) + ) - assert {:ok, %SmartContract{}} = Chain.update_smart_contract(%{address_hash: address.hash}, [], changed_sources) + assert decompiled_smart_contract.decompiled_source_code == code + end + + test "creates two smart contracts for different decompiler versions" do + inserted_decompiled_smart_contract = insert(:decompiled_smart_contract) + code = "code2" + version = "2" - sc_after_call = Repo.get_by(Address, hash: address.hash) |> Repo.preload(:smart_contract_additional_sources) + {:ok, _decompiled_smart_contract} = + Chain.create_decompiled_smart_contract(%{ + decompiler_version: version, + decompiled_source_code: code, + address_hash: inserted_decompiled_smart_contract.address_hash + }) - assert sc_after_call.smart_contract_additional_sources - |> Enum.with_index() - |> Enum.all?(fn {el, ind} -> - {:ok, src} = Enum.fetch(changed_sources, ind) + decompiled_smart_contracts = + Repo.all( + from(ds in DecompiledSmartContract, where: ds.address_hash == ^inserted_decompiled_smart_contract.address_hash) + ) - el.file_name == Map.get(src, :file_name) and - el.contract_source_code == Map.get(src, :contract_source_code) - end) + assert Enum.count(decompiled_smart_contracts) == 2 end end @@ -4816,14 +3997,6 @@ defmodule Explorer.ChainTest do assert Chain.circulating_supply() == ProofOfAuthority.circulating() end - describe "address_hash_to_smart_contract/1" do - test "fetches a smart contract" do - smart_contract = insert(:smart_contract, contract_code_md5: "123") - - assert ^smart_contract = Chain.address_hash_to_smart_contract(smart_contract.address_hash) - end - end - describe "token_from_address_hash/1" do test "with valid hash" do token = insert(:token) @@ -4884,83 +4057,6 @@ defmodule Explorer.ChainTest do end end - describe "stream_not_inserted_token_instances/2" do - test "reduces with given reducer and accumulator for ERC-721 token" do - token_contract_address = insert(:contract_address) - token = insert(:token, contract_address: token_contract_address, type: "ERC-721") - - transaction = - :transaction - |> insert() - |> with_block(insert(:block, number: 1)) - - token_transfer = - insert( - :token_transfer, - block_number: 1000, - to_address: build(:address), - transaction: transaction, - token_contract_address: token_contract_address, - token: token, - token_ids: [11] - ) - - assert {:ok, [result]} = Chain.stream_not_inserted_token_instances([], &[&1 | &2]) - assert result.token_id == List.first(token_transfer.token_ids) - assert result.contract_address_hash == token_transfer.token_contract_address_hash - end - - test "does not fetch token transfers without token_ids" do - token_contract_address = insert(:contract_address) - token = insert(:token, contract_address: token_contract_address, type: "ERC-721") - - transaction = - :transaction - |> insert() - |> with_block(insert(:block, number: 1)) - - insert( - :token_transfer, - block_number: 1000, - to_address: build(:address), - transaction: transaction, - token_contract_address: token_contract_address, - token: token, - token_ids: nil - ) - - assert {:ok, []} = Chain.stream_not_inserted_token_instances([], &[&1 | &2]) - end - - test "do not fetch records with token instances" do - token_contract_address = insert(:contract_address) - token = insert(:token, contract_address: token_contract_address, type: "ERC-721") - - transaction = - :transaction - |> insert() - |> with_block(insert(:block, number: 1)) - - token_transfer = - insert( - :token_transfer, - block_number: 1000, - to_address: build(:address), - transaction: transaction, - token_contract_address: token_contract_address, - token: token, - token_ids: [11] - ) - - insert(:token_instance, - token_id: List.first(token_transfer.token_ids), - token_contract_address_hash: token_transfer.token_contract_address_hash - ) - - assert {:ok, []} = Chain.stream_not_inserted_token_instances([], &[&1 | &2]) - end - end - describe "transaction_has_token_transfers?/1" do test "returns true if transaction has token transfers" do transaction = insert(:transaction) @@ -5293,37 +4389,13 @@ defmodule Explorer.ChainTest do unique_tokens_ids_paginated = token_contract_address.hash - |> Chain.address_to_unique_tokens(paging_options: paging_options) + |> Chain.address_to_unique_tokens(token, paging_options: paging_options) |> Enum.map(& &1.token_id) assert unique_tokens_ids_paginated == [List.first(second_page.token_ids)] end end - describe "uncataloged_token_transfer_block_numbers/0" do - test "returns a list of block numbers" do - block = insert(:block) - address = insert(:address) - - log = - insert(:token_transfer_log, - transaction: - insert(:transaction, - block_number: block.number, - block_hash: block.hash, - cumulative_gas_used: 0, - gas_used: 0, - index: 0 - ), - block: block, - address_hash: address.hash - ) - - block_number = log.block_number - assert {:ok, [^block_number]} = Chain.uncataloged_token_transfer_block_numbers() - end - end - describe "address_to_balances_by_day/1" do test "return a list of balances by day" do address = insert(:address) @@ -5737,433 +4809,4 @@ defmodule Explorer.ChainTest do assert Chain.transaction_to_revert_reason(transaction) == "No credit of that type" end end - - describe "verified_contracts/2" do - test "without contracts" do - assert [] = Chain.verified_contracts() - end - - test "with contracts" do - %SmartContract{address_hash: hash} = insert(:smart_contract) - - assert [%SmartContract{address_hash: ^hash}] = Chain.verified_contracts() - end - - test "with contracts can be paginated" do - second_page_contracts_ids = - 50 - |> insert_list(:smart_contract) - |> Enum.map(& &1.id) - - contract = insert(:smart_contract) - - assert second_page_contracts_ids == - [paging_options: %PagingOptions{key: {contract.id}, page_size: 50}] - |> Chain.verified_contracts() - |> Enum.map(& &1.id) - |> Enum.reverse() - end - - test "filters solidity" do - insert(:smart_contract, is_vyper_contract: true) - %SmartContract{address_hash: hash} = insert(:smart_contract, is_vyper_contract: false) - - assert [%SmartContract{address_hash: ^hash}] = Chain.verified_contracts(filter: :solidity) - end - - test "filters vyper" do - insert(:smart_contract, is_vyper_contract: false) - %SmartContract{address_hash: hash} = insert(:smart_contract, is_vyper_contract: true) - - assert [%SmartContract{address_hash: ^hash}] = Chain.verified_contracts(filter: :vyper) - end - - test "search by address" do - insert(:smart_contract) - insert(:smart_contract) - insert(:smart_contract) - %SmartContract{address_hash: hash} = insert(:smart_contract) - - assert [%SmartContract{address_hash: ^hash}] = Chain.verified_contracts(search: Hash.to_string(hash)) - end - - test "search by name" do - insert(:smart_contract) - insert(:smart_contract) - insert(:smart_contract) - contract_name = "qwertyufhgkhiop" - %SmartContract{address_hash: hash} = insert(:smart_contract, name: contract_name) - - assert [%SmartContract{address_hash: ^hash}] = Chain.verified_contracts(search: contract_name) - end - end - - describe "proxy contracts features" do - @proxy_abi [ - %{ - "type" => "function", - "stateMutability" => "nonpayable", - "payable" => false, - "outputs" => [%{"type" => "bool", "name" => ""}], - "name" => "upgradeTo", - "inputs" => [%{"type" => "address", "name" => "newImplementation"}], - "constant" => false - }, - %{ - "type" => "function", - "stateMutability" => "view", - "payable" => false, - "outputs" => [%{"type" => "uint256", "name" => ""}], - "name" => "version", - "inputs" => [], - "constant" => true - }, - %{ - "type" => "function", - "stateMutability" => "view", - "payable" => false, - "outputs" => [%{"type" => "address", "name" => ""}], - "name" => "implementation", - "inputs" => [], - "constant" => true - }, - %{ - "type" => "function", - "stateMutability" => "nonpayable", - "payable" => false, - "outputs" => [], - "name" => "renounceOwnership", - "inputs" => [], - "constant" => false - }, - %{ - "type" => "function", - "stateMutability" => "view", - "payable" => false, - "outputs" => [%{"type" => "address", "name" => ""}], - "name" => "getOwner", - "inputs" => [], - "constant" => true - }, - %{ - "type" => "function", - "stateMutability" => "view", - "payable" => false, - "outputs" => [%{"type" => "address", "name" => ""}], - "name" => "getProxyStorage", - "inputs" => [], - "constant" => true - }, - %{ - "type" => "function", - "stateMutability" => "nonpayable", - "payable" => false, - "outputs" => [], - "name" => "transferOwnership", - "inputs" => [%{"type" => "address", "name" => "_newOwner"}], - "constant" => false - }, - %{ - "type" => "constructor", - "stateMutability" => "nonpayable", - "payable" => false, - "inputs" => [ - %{"type" => "address", "name" => "_proxyStorage"}, - %{"type" => "address", "name" => "_implementationAddress"} - ] - }, - %{"type" => "fallback", "stateMutability" => "nonpayable", "payable" => false}, - %{ - "type" => "event", - "name" => "Upgraded", - "inputs" => [ - %{"type" => "uint256", "name" => "version", "indexed" => false}, - %{"type" => "address", "name" => "implementation", "indexed" => true} - ], - "anonymous" => false - }, - %{ - "type" => "event", - "name" => "OwnershipRenounced", - "inputs" => [%{"type" => "address", "name" => "previousOwner", "indexed" => true}], - "anonymous" => false - }, - %{ - "type" => "event", - "name" => "OwnershipTransferred", - "inputs" => [ - %{"type" => "address", "name" => "previousOwner", "indexed" => true}, - %{"type" => "address", "name" => "newOwner", "indexed" => true} - ], - "anonymous" => false - } - ] - - @implementation_abi [ - %{ - "constant" => false, - "inputs" => [%{"name" => "x", "type" => "uint256"}], - "name" => "set", - "outputs" => [], - "payable" => false, - "stateMutability" => "nonpayable", - "type" => "function" - }, - %{ - "constant" => true, - "inputs" => [], - "name" => "get", - "outputs" => [%{"name" => "", "type" => "uint256"}], - "payable" => false, - "stateMutability" => "view", - "type" => "function" - } - ] - - test "combine_proxy_implementation_abi/2 returns empty [] abi if proxy abi is null" do - proxy_contract_address = insert(:contract_address) - - assert Chain.combine_proxy_implementation_abi(%SmartContract{address_hash: proxy_contract_address.hash, abi: nil}) == - [] - end - - test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") - - get_eip1967_implementation() - - assert Chain.combine_proxy_implementation_abi(smart_contract) == [] - end - - test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - - assert Chain.combine_proxy_implementation_abi(smart_contract) == @proxy_abi - end - - test "combine_proxy_implementation_abi/2 returns proxy + implementation abi if implementation is verified" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - - implementation_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: implementation_contract_address.hash, - abi: @implementation_abi, - contract_code_md5: "123" - ) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x000000000000000000000000" <> implementation_contract_address_hash_string - } - ]} - end - ) - - combined_abi = Chain.combine_proxy_implementation_abi(smart_contract) - - assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == false - assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == false - assert Enum.any?(combined_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == true - assert Enum.any?(combined_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == true - end - - test "get_implementation_abi_from_proxy/2 returns empty [] abi if proxy abi is null" do - proxy_contract_address = insert(:contract_address) - - assert Chain.get_implementation_abi_from_proxy( - %SmartContract{address_hash: proxy_contract_address.hash, abi: nil}, - [] - ) == - [] - end - - test "get_implementation_abi_from_proxy/2 returns [] abi for unverified proxy" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") - - get_eip1967_implementation() - - assert Chain.combine_proxy_implementation_abi(smart_contract) == [] - end - - test "get_implementation_abi_from_proxy/2 returns [] if implementation is not verified" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - - assert Chain.get_implementation_abi_from_proxy(smart_contract, []) == [] - end - - test "get_implementation_abi_from_proxy/2 returns implementation abi if implementation is verified" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - - implementation_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: implementation_contract_address.hash, - abi: @implementation_abi, - contract_code_md5: "123" - ) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x000000000000000000000000" <> implementation_contract_address_hash_string - } - ]} - end - ) - - implementation_abi = Chain.get_implementation_abi_from_proxy(smart_contract, []) - - assert implementation_abi == @implementation_abi - end - - test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") - - implementation_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: implementation_contract_address.hash, - abi: @implementation_abi, - contract_code_md5: "123" - ) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn %{ - id: _id, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x000000000000000000000000" <> implementation_contract_address_hash_string} - end - ) - - implementation_abi = Chain.get_implementation_abi_from_proxy(smart_contract, []) - - assert implementation_abi == @implementation_abi - end - - test "get_implementation_abi/1 returns empty [] abi if implementation address is null" do - assert Chain.get_implementation_abi(nil) == [] - end - - test "get_implementation_abi/1 returns [] if implementation is not verified" do - implementation_contract_address = insert(:contract_address) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - assert Chain.get_implementation_abi("0x" <> implementation_contract_address_hash_string) == [] - end - - test "get_implementation_abi/1 returns implementation abi if implementation is verified" do - proxy_contract_address = insert(:contract_address) - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - - implementation_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: implementation_contract_address.hash, - abi: @implementation_abi, - contract_code_md5: "123" - ) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - implementation_abi = Chain.get_implementation_abi("0x" <> implementation_contract_address_hash_string) - - assert implementation_abi == @implementation_abi - end - end - - def get_eip1967_implementation do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, - _options -> - {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} - end) - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", - "latest" - ] - }, - _options -> - {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} - end) - |> expect(:json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", - "latest" - ] - }, - _options -> - {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} - end) - end end diff --git a/apps/explorer/test/explorer/counters/average_block_time_test.exs b/apps/explorer/test/explorer/counters/average_block_time_test.exs index d9e0cfb35cd4..3472276d1959 100644 --- a/apps/explorer/test/explorer/counters/average_block_time_test.exs +++ b/apps/explorer/test/explorer/counters/average_block_time_test.exs @@ -129,5 +129,55 @@ defmodule Explorer.Counters.AverageBlockTimeTest do assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT3S") end + + test "timestamps are compared correctly" do + block_number = 99_999_999 + + first_timestamp = ~U[2023-08-23 19:04:59.000000Z] + pseudo_after_timestamp = ~U[2022-08-23 19:05:59.000000Z] + + insert(:block, number: block_number, consensus: true, timestamp: pseudo_after_timestamp) + insert(:block, number: block_number + 1, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 3)) + insert(:block, number: block_number + 2, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 6)) + + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + i + 2, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 9) + ) + end) + + AverageBlockTime.refresh() + + %{timestamps: timestamps} = :sys.get_state(AverageBlockTime) + + assert Enum.sort_by(timestamps, fn {_bn, ts} -> ts end, &>=/2) == timestamps + end + + test "average time are calculated correctly for blocks that are not in chronological order" do + block_number = 99_999_999 + + first_timestamp = Timex.now() + + insert(:block, number: block_number, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 3)) + insert(:block, number: block_number + 1, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 6)) + insert(:block, number: block_number + 2, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 9)) + insert(:block, number: block_number + 3, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: -69)) + insert(:block, number: block_number + 4, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: -66)) + insert(:block, number: block_number + 5, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: -63)) + + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + i + 5, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 9) + ) + end) + + AverageBlockTime.refresh() + + assert Timex.Duration.to_milliseconds(AverageBlockTime.average_block_time()) == 3000 + end end end diff --git a/apps/explorer/test/explorer/counters/fresh_pending_transactions_counter_test.exs b/apps/explorer/test/explorer/counters/fresh_pending_transactions_counter_test.exs new file mode 100644 index 000000000000..6a93612e0485 --- /dev/null +++ b/apps/explorer/test/explorer/counters/fresh_pending_transactions_counter_test.exs @@ -0,0 +1,27 @@ +defmodule Explorer.Counters.FreshPendingTransactionsCounterTest do + use Explorer.DataCase + + alias Explorer.Counters.FreshPendingTransactionsCounter + + test "populates the cache with the number of pending transactions addresses" do + insert(:transaction) + insert(:transaction) + insert(:transaction) + + start_supervised!(FreshPendingTransactionsCounter) + FreshPendingTransactionsCounter.consolidate() + + assert FreshPendingTransactionsCounter.fetch([]) == Decimal.new("3") + end + + test "count only fresh transactions" do + insert(:transaction, inserted_at: Timex.shift(Timex.now(), hours: -2)) + insert(:transaction) + insert(:transaction) + + start_supervised!(FreshPendingTransactionsCounter) + FreshPendingTransactionsCounter.consolidate() + + assert FreshPendingTransactionsCounter.fetch([]) == Decimal.new("2") + end +end diff --git a/apps/explorer/test/explorer/counters/last_output_root_size_counter_test.exs b/apps/explorer/test/explorer/counters/last_output_root_size_counter_test.exs new file mode 100644 index 000000000000..4f00da7cb23a --- /dev/null +++ b/apps/explorer/test/explorer/counters/last_output_root_size_counter_test.exs @@ -0,0 +1,47 @@ +defmodule Explorer.Counters.LastOutputRootSizeCounterTest do + use Explorer.DataCase + + alias Explorer.Counters.LastOutputRootSizeCounter + + if Application.compile_env(:explorer, :chain_type) == "optimism" do + test "populates the cache with the number of transactions in last output root" do + first_block = insert(:block) + + insert(:op_output_root, l2_block_number: first_block.number) + + second_block = insert(:block, number: first_block.number + 10) + insert(:op_output_root, l2_block_number: second_block.number) + + insert(:transaction) |> with_block(first_block) + insert(:transaction) |> with_block(second_block) + insert(:transaction) |> with_block(second_block) + + start_supervised!(LastOutputRootSizeCounter) + LastOutputRootSizeCounter.consolidate() + + assert LastOutputRootSizeCounter.fetch([]) == Decimal.new("2") + end + + test "does not count transactions that are not in output root yet" do + first_block = insert(:block) + + insert(:op_output_root, l2_block_number: first_block.number) + + second_block = insert(:block, number: first_block.number + 10) + insert(:op_output_root, l2_block_number: second_block.number) + + insert(:transaction) |> with_block(first_block) + insert(:transaction) |> with_block(second_block) + insert(:transaction) |> with_block(second_block) + + third_block = insert(:block, number: second_block.number + 1) + insert(:transaction) |> with_block(third_block) + insert(:transaction) |> with_block(third_block) + + start_supervised!(LastOutputRootSizeCounter) + LastOutputRootSizeCounter.consolidate() + + assert LastOutputRootSizeCounter.fetch([]) == Decimal.new("2") + end + end +end diff --git a/apps/explorer/test/explorer/counters/transactions_24h_stats_test.exs b/apps/explorer/test/explorer/counters/transactions_24h_stats_test.exs new file mode 100644 index 000000000000..96a5fccaffe3 --- /dev/null +++ b/apps/explorer/test/explorer/counters/transactions_24h_stats_test.exs @@ -0,0 +1,61 @@ +defmodule Explorer.Counters.Transactions24hStatsTest do + use Explorer.DataCase + + alias Explorer.Counters.Transactions24hStats + + test "populates the cache with transaction counters" do + block = insert(:block, base_fee_per_gas: 50) + address = insert(:address) + + # fee = 10000 + + insert(:transaction, + from_address: address, + block: block, + block_number: block.number, + cumulative_gas_used: 0, + index: 0, + gas_price: 100, + gas_used: 100 + ) + + # fee = 15000 + + insert(:transaction, + from_address: address, + block: block, + block_number: block.number, + cumulative_gas_used: 100, + index: 1, + gas_price: 150, + gas_used: 100, + max_priority_fee_per_gas: 100, + max_fee_per_gas: 200 + ) + + # fee = 10000 + + insert(:transaction, + from_address: address, + block: block, + block_number: block.number, + cumulative_gas_used: 200, + index: 2, + gas_price: 100, + gas_used: 100, + max_priority_fee_per_gas: 70, + max_fee_per_gas: 100 + ) + + start_supervised!(Transactions24hStats) + Transactions24hStats.consolidate() + + transaction_count = Transactions24hStats.fetch_count([]) + transaction_fee_sum = Transactions24hStats.fetch_fee_sum([]) + transaction_fee_average = Transactions24hStats.fetch_fee_average([]) + + assert transaction_count == Decimal.new("3") + assert transaction_fee_sum == Decimal.new("35000") + assert transaction_fee_average == Decimal.new("11667") + end +end diff --git a/apps/explorer/test/explorer/etherscan/logs_test.exs b/apps/explorer/test/explorer/etherscan/logs_test.exs index 490dce199dc6..b5953407f83c 100644 --- a/apps/explorer/test/explorer/etherscan/logs_test.exs +++ b/apps/explorer/test/explorer/etherscan/logs_test.exs @@ -6,6 +6,25 @@ defmodule Explorer.Etherscan.LogsTest do alias Explorer.Etherscan.Logs alias Explorer.Chain.Transaction + @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" + @first_topic_hex_string_2 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + @first_topic_hex_string_3 "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" + + @second_topic_hex_string_1 "0x00000000000000000000000098a9dc37d3650b5b30d6c12789b3881ee0b70c16" + @second_topic_hex_string_2 "0x000000000000000000000000e2680fd7cdbb04e9087a647ad4d023ef6c8fb4e2" + @second_topic_hex_string_3 "0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168" + + @third_topic_hex_string_1 "0x0000000000000000000000005079fc00f00f30000e0c8c083801cfde000008b6" + @third_topic_hex_string_2 "0x000000000000000000000000e2680fd7cdbb04e9087a647ad4d023ef6c8fb4e2" + @third_topic_hex_string_3 "0x0000000000000000000000000f6d9bd6fc315bbf95b5c44f4eba2b2762f8c372" + + @fourth_topic_hex_string_1 "0x8c9b7729443a4444242342b2ca385a239a5c1d76a88473e1cd2ab0c70dd1b9c7" + + defp topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end + describe "list_logs/1" do test "with empty db" do contract_address = build(:contract_address) @@ -38,14 +57,15 @@ defmodule Explorer.Etherscan.LogsTest do test "with address with one log response includes all required information" do contract_address = insert(:contract_address) + block = insert(:block) transaction = - %Transaction{block: block} = + %Transaction{} = :transaction - |> insert(to_address: contract_address) - |> with_block() + |> insert(to_address: contract_address, block_timestamp: block.timestamp) + |> with_block(block) - log = insert(:log, address: contract_address, transaction: transaction) + log = insert(:log, address: contract_address, block_number: block.number, transaction: transaction) filter = %{ from_block: block.number, @@ -79,7 +99,7 @@ defmodule Explorer.Etherscan.LogsTest do |> insert(to_address: contract_address) |> with_block() - insert_list(2, :log, address: contract_address, transaction: transaction) + insert_list(2, :log, address: contract_address, transaction: transaction, block_number: block.number) filter = %{ from_block: block.number, @@ -110,8 +130,8 @@ defmodule Explorer.Etherscan.LogsTest do |> insert(to_address: contract_address) |> with_block(second_block) - insert(:log, address: contract_address, transaction: transaction_block1) - insert(:log, address: contract_address, transaction: transaction_block2) + insert(:log, address: contract_address, transaction: transaction_block1, block_number: first_block.number) + insert(:log, address: contract_address, transaction: transaction_block2, block_number: second_block.number) filter = %{ from_block: second_block.number, @@ -143,8 +163,8 @@ defmodule Explorer.Etherscan.LogsTest do |> insert(to_address: contract_address) |> with_block(second_block) - insert(:log, address: contract_address, transaction: transaction_block1) - insert(:log, address: contract_address, transaction: transaction_block2) + insert(:log, address: contract_address, transaction: transaction_block1, block_number: first_block.number) + insert(:log, address: contract_address, transaction: transaction_block2, block_number: second_block.number) filter = %{ from_block: first_block.number, @@ -167,7 +187,8 @@ defmodule Explorer.Etherscan.LogsTest do |> insert(to_address: contract_address) |> with_block() - inserted_records = insert_list(2000, :log, address: contract_address, transaction: transaction) + inserted_records = + insert_list(2000, :log, address: contract_address, transaction: transaction, block_number: block.number) filter = %{ from_block: block.number, @@ -183,7 +204,6 @@ defmodule Explorer.Etherscan.LogsTest do next_page_params = %{ log_index: last_record.index, - transaction_index: last_record.transaction_index, block_number: transaction.block_number } @@ -210,13 +230,13 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic" + first_topic: topic(@first_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some other topic" + first_topic: topic(@first_topic_hex_string_2) ] log1 = insert(:log, log1_details) @@ -246,15 +266,15 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic", - second_topic: "some second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic", - second_topic: "some OTHER second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_2) ] _log1 = insert(:log, log1_details) @@ -287,15 +307,15 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic", - second_topic: "some second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1) ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some OTHER first topic", - second_topic: "some OTHER second topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2) ] log1 = insert(:log, log1_details) @@ -326,13 +346,15 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic" + first_topic: topic(@first_topic_hex_string_1), + block_number: block.number ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some OTHER first topic" + first_topic: topic(@first_topic_hex_string_2), + block_number: block.number ] _log1 = insert(:log, log1_details) @@ -363,15 +385,17 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic", - second_topic: "some second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block_number: block.number ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some OTHER first topic", - second_topic: "some OTHER second topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2), + block_number: block.number ] _log1 = insert(:log, log1_details) @@ -404,25 +428,28 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic", - second_topic: "some second topic", - third_topic: "some third topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + block_number: block.number ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some OTHER first topic", - second_topic: "some OTHER second topic", - third_topic: "some OTHER third topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2), + third_topic: topic(@third_topic_hex_string_2), + block_number: block.number ] log3_details = [ address: contract_address, transaction: transaction, - first_topic: "some ALT first topic", - second_topic: "some ALT second topic", - third_topic: "some ALT third topic" + first_topic: topic(@first_topic_hex_string_3), + second_topic: topic(@second_topic_hex_string_3), + third_topic: topic(@third_topic_hex_string_3), + block_number: block.number ] _log1 = insert(:log, log1_details) @@ -461,25 +488,28 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some first topic", - second_topic: "some second topic", - third_topic: "some third topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + block_number: block.number ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some OTHER first topic", - second_topic: "some OTHER second topic", - third_topic: "some OTHER third topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2), + third_topic: topic(@third_topic_hex_string_2), + block_number: block.number ] log3_details = [ address: contract_address, transaction: transaction, - first_topic: "some ALT first topic", - second_topic: "some ALT second topic", - third_topic: "some ALT third topic" + first_topic: topic(@first_topic_hex_string_3), + second_topic: topic(@second_topic_hex_string_3), + third_topic: topic(@third_topic_hex_string_3), + block_number: block.number ] log1 = insert(:log, log1_details) @@ -518,25 +548,28 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic", - third_topic: "some third topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + block_number: block.number ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some OTHER second topic", - third_topic: "some third topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_2), + third_topic: topic(@third_topic_hex_string_1), + block_number: block.number ] log3_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic", - third_topic: "some third topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + block_number: block.number ] log1 = insert(:log, log1_details) @@ -575,26 +608,29 @@ defmodule Explorer.Etherscan.LogsTest do log1_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + block_number: block.number ] log2_details = [ address: contract_address, transaction: transaction, - first_topic: "some OTHER topic", - second_topic: "some OTHER second topic", - third_topic: "some OTHER third topic", - fourth_topic: "some fourth topic" + first_topic: topic(@first_topic_hex_string_2), + second_topic: topic(@second_topic_hex_string_2), + third_topic: topic(@third_topic_hex_string_2), + fourth_topic: topic(@fourth_topic_hex_string_1), + block_number: block.number ] log3_details = [ address: contract_address, transaction: transaction, - first_topic: "some topic", - second_topic: "some second topic", - third_topic: "some third topic", - fourth_topic: "some fourth topic" + first_topic: topic(@first_topic_hex_string_1), + second_topic: topic(@second_topic_hex_string_1), + third_topic: topic(@third_topic_hex_string_1), + fourth_topic: topic(@fourth_topic_hex_string_1), + block_number: block.number ] log1 = insert(:log, log1_details) diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index 02cc0ced53cb..37b6e685d618 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -159,11 +159,12 @@ defmodule Explorer.EtherscanTest do test "loads block_timestamp" do address = insert(:address) + block = insert(:block) - %Transaction{block: block} = + %Transaction{} = :transaction - |> insert(from_address: address) - |> with_block() + |> insert(from_address: address, block_timestamp: block.timestamp) + |> with_block(block) [found_transaction] = Etherscan.list_transactions(address.hash) @@ -293,8 +294,8 @@ defmodule Explorer.EtherscanTest do end options = %{ - start_block: second_block.number, - end_block: third_block.number + startblock: second_block.number, + endblock: third_block.number } found_transactions = Etherscan.list_transactions(address.hash, options) @@ -308,7 +309,7 @@ defmodule Explorer.EtherscanTest do end end - test "with start_block but no end_block option" do + test "with startblock but no endblock option" do blocks = [_, _, third_block, fourth_block] = insert_list(4, :block) address = insert(:address) @@ -319,7 +320,7 @@ defmodule Explorer.EtherscanTest do end options = %{ - start_block: third_block.number + startblock: third_block.number } found_transactions = Etherscan.list_transactions(address.hash, options) @@ -333,7 +334,7 @@ defmodule Explorer.EtherscanTest do end end - test "with end_block but no start_block option" do + test "with endblock but no startblock option" do blocks = [first_block, second_block, _, _] = insert_list(4, :block) address = insert(:address) @@ -344,7 +345,7 @@ defmodule Explorer.EtherscanTest do end options = %{ - end_block: second_block.number + endblock: second_block.number } found_transactions = Etherscan.list_transactions(address.hash, options) @@ -370,7 +371,7 @@ defmodule Explorer.EtherscanTest do for block <- Enum.concat([blocks1, blocks2, blocks3]) do 2 - |> insert_list(:transaction, from_address: address) + |> insert_list(:transaction, from_address: address, block_timestamp: block.timestamp) |> with_block(block) end @@ -629,7 +630,7 @@ defmodule Explorer.EtherscanTest do transaction = :transaction - |> insert(from_address: address, to_address: nil) + |> insert(from_address: address, to_address: nil, block_timestamp: block.timestamp) |> with_contract_creation(contract_address) |> with_block(block) @@ -972,8 +973,8 @@ defmodule Explorer.EtherscanTest do end options = %{ - start_block: second_block.number, - end_block: third_block.number + startblock: second_block.number, + endblock: third_block.number } found_internal_transactions = Etherscan.list_internal_transactions(address.hash, options) @@ -1115,11 +1116,13 @@ defmodule Explorer.EtherscanTest do end test "returns all required fields" do + block = insert(:block) + transaction = %{block: block} = :transaction - |> insert() - |> with_block() + |> insert(block_timestamp: block.timestamp) + |> with_block(block) token_transfer = insert(:token_transfer, @@ -1362,8 +1365,8 @@ defmodule Explorer.EtherscanTest do end options = %{ - start_block: second_block.number, - end_block: third_block.number + startblock: second_block.number, + endblock: third_block.number } found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) @@ -1377,7 +1380,7 @@ defmodule Explorer.EtherscanTest do end end - test "with start_block but no end_block option" do + test "with startblock but no endblock option" do blocks = [_, _, third_block, fourth_block] = insert_list(4, :block) address = insert(:address) @@ -1395,7 +1398,7 @@ defmodule Explorer.EtherscanTest do ) end - options = %{start_block: third_block.number} + options = %{startblock: third_block.number} found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) @@ -1408,7 +1411,7 @@ defmodule Explorer.EtherscanTest do end end - test "with end_block but no start_block option" do + test "with endblock but no startblock option" do blocks = [first_block, second_block, _, _] = insert_list(4, :block) address = insert(:address) @@ -1426,7 +1429,7 @@ defmodule Explorer.EtherscanTest do ) end - options = %{end_block: second_block.number} + options = %{endblock: second_block.number} found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) diff --git a/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs b/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs index 953a5b5a1640..e23e64f07744 100644 --- a/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs +++ b/apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs @@ -1,5 +1,5 @@ defmodule Explorer.ExchangeRatesTest do - use ExUnit.Case, async: false + use Explorer.DataCase import Mox @@ -7,12 +7,16 @@ defmodule Explorer.ExchangeRatesTest do alias Explorer.ExchangeRates alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Source.TestSource + alias Explorer.Market.MarketHistoryCache @moduletag :capture_log setup :verify_on_exit! setup do + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()}) + # Use TestSource mock and ets table for this test set source_configuration = Application.get_env(:explorer, Explorer.ExchangeRates.Source) rates_configuration = Application.get_env(:explorer, Explorer.ExchangeRates) @@ -24,6 +28,8 @@ defmodule Explorer.ExchangeRatesTest do on_exit(fn -> Application.put_env(:explorer, Explorer.ExchangeRates.Source, source_configuration) Application.put_env(:explorer, Explorer.ExchangeRates, rates_configuration) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) end) end @@ -79,7 +85,8 @@ defmodule Explorer.ExchangeRatesTest do name: "test_name", symbol: "test_symbol", usd_value: Decimal.new("1.0"), - volume_24h_usd: Decimal.new("1000.0") + volume_24h_usd: Decimal.new("1000.0"), + image_url: nil } expected_symbol = expected_token.symbol diff --git a/apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs b/apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs index 044c84553f3e..aa17f904019f 100644 --- a/apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs +++ b/apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs @@ -71,7 +71,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do end test "composes cg url to list of contract address hashes" do - assert "https://api.coingecko.com/api/v3/simple/token_price/ethereum?vs_currencies=usd&include_market_cap=true&contract_addresses=0xdAC17F958D2ee523a2206206994597C13D831ec7" == + assert "https://api.coingecko.com/api/v3/simple/token_price/ethereum?vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&contract_addresses=0xdAC17F958D2ee523a2206206994597C13D831ec7" == CoinGecko.source_url(["0xdAC17F958D2ee523a2206206994597C13D831ec7"]) end @@ -120,7 +120,8 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do name: "POA Network", symbol: "POA", usd_value: Decimal.new("0.01345698"), - volume_24h_usd: Decimal.new("119946") + volume_24h_usd: Decimal.new("119946"), + image_url: "https://assets.coingecko.com/coins/images/3157/thumb/poa-network.png?1548331565" } ] diff --git a/apps/explorer/test/explorer/exchange_rates/token_exchange_rates_test.exs b/apps/explorer/test/explorer/exchange_rates/token_exchange_rates_test.exs index 2e5f9ee1a6c6..a6d3255a8047 100644 --- a/apps/explorer/test/explorer/exchange_rates/token_exchange_rates_test.exs +++ b/apps/explorer/test/explorer/exchange_rates/token_exchange_rates_test.exs @@ -77,7 +77,9 @@ defmodule Explorer.TokenExchangeRatesTest do "GET", "/simple/token_price/ethereum", fn conn -> - assert conn.query_string == "vs_currencies=usd&include_market_cap=true&contract_addresses=#{joined_addresses}" + assert conn.query_string == + "vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&contract_addresses=#{joined_addresses}" + Conn.resp(conn, 200, Jason.encode!(token_exchange_rates)) end ) @@ -159,7 +161,9 @@ defmodule Explorer.TokenExchangeRatesTest do "GET", "/simple/token_price/ethereum", fn conn -> - assert conn.query_string == "vs_currencies=usd&include_market_cap=true&contract_addresses=#{joined_addresses}" + assert conn.query_string == + "vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&contract_addresses=#{joined_addresses}" + Conn.resp(conn, 200, "{}") end ) @@ -239,7 +243,9 @@ defmodule Explorer.TokenExchangeRatesTest do "GET", "/simple/token_price/ethereum", fn conn -> - assert conn.query_string == "vs_currencies=usd&include_market_cap=true&contract_addresses=#{joined_addresses}" + assert conn.query_string == + "vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&contract_addresses=#{joined_addresses}" + Conn.resp(conn, 429, "Too many requests") end ) diff --git a/apps/explorer/test/explorer/market/history/cataloger_test.exs b/apps/explorer/test/explorer/market/history/cataloger_test.exs index f7a86a960e2c..8e784ed0dc0c 100644 --- a/apps/explorer/test/explorer/market/history/cataloger_test.exs +++ b/apps/explorer/test/explorer/market/history/cataloger_test.exs @@ -54,13 +54,17 @@ defmodule Explorer.Market.History.CatalogerTest do """ Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, resp) end) - records = [%{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5)}] - expect(TestSource, :fetch_price_history, fn 1 -> {:ok, records} end) + + records = [ + %{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5), secondary_coin: false} + ] + + expect(TestSource, :fetch_price_history, fn 1, _ -> {:ok, records} end) set_mox_global() state = %{} assert {:noreply, state} == Cataloger.handle_info({:fetch_price_history, 1}, state) - assert_receive {_ref, {:price_history, {1, 0, {:ok, ^records}}}} + assert_receive {_ref, {:price_history, {1, 0, false, {:ok, ^records}}}} end test "handle_info with successful tasks (price, market cap and tvl)" do @@ -80,15 +84,15 @@ defmodule Explorer.Market.History.CatalogerTest do state2 = Map.put(state, :market_cap_records, market_cap_records) - state3 = Map.put(state2, :tvl_records, tvl_records) + assert {:noreply, state} == + Cataloger.handle_info({nil, {:price_history, {1, 0, false, {:ok, price_records}}}}, state) - assert {:noreply, state} == Cataloger.handle_info({nil, {:price_history, {1, 0, {:ok, price_records}}}}, state) - assert_receive {:fetch_market_cap_history, 365} + assert_receive {:fetch_market_cap_history, 1} assert {:noreply, state2} == Cataloger.handle_info({nil, {:market_cap_history, {0, 3, {:ok, market_cap_records}}}}, state) - assert {:noreply, state3} == + assert {:noreply, %{}} == Cataloger.handle_info({nil, {:tvl_history, {0, 3, {:ok, tvl_records}}}}, state2) assert record2 = Repo.get_by(MarketHistory, date: Enum.at(price_records, 1).date) @@ -113,15 +117,15 @@ defmodule Explorer.Market.History.CatalogerTest do state2 = Map.put(state, :market_cap_records, market_cap_records) - state3 = Map.put(state2, :tvl_records, []) + assert {:noreply, state} == + Cataloger.handle_info({nil, {:price_history, {1, 0, false, {:ok, price_records}}}}, state) - assert {:noreply, state} == Cataloger.handle_info({nil, {:price_history, {1, 0, {:ok, price_records}}}}, state) - assert_receive {:fetch_market_cap_history, 365} + assert_receive {:fetch_market_cap_history, 1} assert {:noreply, state2} == Cataloger.handle_info({nil, {:market_cap_history, {0, 3, {:ok, market_cap_records}}}}, state) - assert {:noreply, state3} == + assert {:noreply, %{}} == Cataloger.handle_info({nil, {:tvl_history, {0, 3, {:ok, tvl_records}}}}, state2) assert record = Repo.get_by(MarketHistory, date: Enum.at(price_records, 0).date) @@ -142,15 +146,15 @@ defmodule Explorer.Market.History.CatalogerTest do state2 = Map.put(state, :market_cap_records, market_cap_records) - state3 = Map.put(state2, :tvl_records, tvl_records) + assert {:noreply, state} == + Cataloger.handle_info({nil, {:price_history, {1, 0, false, {:ok, price_records}}}}, state) - assert {:noreply, state} == Cataloger.handle_info({nil, {:price_history, {1, 0, {:ok, price_records}}}}, state) - assert_receive {:fetch_market_cap_history, 365} + assert_receive {:fetch_market_cap_history, 1} assert {:noreply, state2} == Cataloger.handle_info({nil, {:market_cap_history, {0, 3, {:ok, market_cap_records}}}}, state) - assert {:noreply, state3} == + assert {:noreply, %{}} == Cataloger.handle_info({nil, {:tvl_history, {0, 3, {:ok, tvl_records}}}}, state2) assert record = Repo.get_by(MarketHistory, date: Enum.at(price_records, 0).date) @@ -159,6 +163,72 @@ defmodule Explorer.Market.History.CatalogerTest do assert record.tvl == nil end + test "current day values are saved in state" do + bypass = Bypass.open() + Application.put_env(:explorer, CryptoCompare, base_url: "http://localhost:#{bypass.port}") + old_env = Application.get_all_env(:explorer) + + Application.put_env(:explorer, Explorer.History.Process, base_backoff: 0) + + resp = + &""" + { + "Response": "Success", + "Type": 100, + "Aggregated": false, + "TimeTo": 1522569618, + "TimeFrom": 1522566018, + "FirstValueInArray": true, + "ConversionType": { + "type": "multiply", + "conversionSymbol": "ETH" + }, + "Data": [{ + "time": #{&1}, + "high": 10, + "low": 5, + "open": 5, + "volumefrom": 0, + "volumeto": 0, + "close": #{&2}, + "conversionType": "multiply", + "conversionSymbol": "ETH" + }], + "RateLimit": {}, + "HasWarning": false + } + """ + + Bypass.expect(bypass, fn conn -> + case conn.params["limit"] do + "365" -> Conn.resp(conn, 200, resp.(1_522_566_018, 10)) + _ -> Conn.resp(conn, 200, resp.(1_522_633_818, 20)) + end + end) + + {:ok, pid} = Cataloger.start_link([]) + + :timer.sleep(4000) + + Process.send(pid, {:fetch_price_history, 1}, []) + + :timer.sleep(4000) + + assert [ + %Explorer.Market.MarketHistory{ + date: ~D[2018-04-01] + } = first_entry, + %Explorer.Market.MarketHistory{ + date: ~D[2018-04-02] + } = second_entry + ] = MarketHistory |> Repo.all() + + assert Decimal.eq?(first_entry.closing_price, Decimal.new(10)) + assert Decimal.eq?(second_entry.closing_price, Decimal.new(20)) + + Application.put_all_env(explorer: old_env) + end + test "handle info for DOWN message" do assert {:noreply, %{}} == Cataloger.handle_info({:DOWN, nil, :process, nil, nil}, %{}) end diff --git a/apps/explorer/test/explorer/market/history/source/price/crypto_compare_test.exs b/apps/explorer/test/explorer/market/history/source/price/crypto_compare_test.exs index 2d8311b0ae1f..82f60c12d92a 100644 --- a/apps/explorer/test/explorer/market/history/source/price/crypto_compare_test.exs +++ b/apps/explorer/test/explorer/market/history/source/price/crypto_compare_test.exs @@ -63,28 +63,31 @@ defmodule Explorer.Market.History.Source.Price.CryptoCompareTest do %{ closing_price: Decimal.from_float(9655.77), date: ~D[2018-04-24], - opening_price: Decimal.from_float(8967.86) + opening_price: Decimal.from_float(8967.86), + secondary_coin: false }, %{ closing_price: Decimal.from_float(8873.62), date: ~D[2018-04-25], - opening_price: Decimal.from_float(9657.69) + opening_price: Decimal.from_float(9657.69), + secondary_coin: false }, %{ closing_price: Decimal.from_float(8804.32), date: ~D[2018-04-26], - opening_price: Decimal.from_float(8873.57) + opening_price: Decimal.from_float(8873.57), + secondary_coin: false } ] - assert {:ok, expected} == CryptoCompare.fetch_price_history(3) + assert {:ok, expected} == CryptoCompare.fetch_price_history(3, false) end test "with errored request", %{bypass: bypass} do error_text = ~S({"error": "server error"}) Bypass.expect(bypass, fn conn -> Conn.resp(conn, 500, error_text) end) - assert :error == CryptoCompare.fetch_price_history(3) + assert :error == CryptoCompare.fetch_price_history(3, false) end test "rejects empty prices", %{bypass: bypass} do @@ -135,10 +138,15 @@ defmodule Explorer.Market.History.Source.Price.CryptoCompareTest do Bypass.expect(bypass, fn conn -> Conn.resp(conn, 200, json) end) expected = [ - %{closing_price: Decimal.from_float(8804.32), date: ~D[2018-04-26], opening_price: Decimal.from_float(8873.57)} + %{ + closing_price: Decimal.from_float(8804.32), + date: ~D[2018-04-26], + opening_price: Decimal.from_float(8873.57), + secondary_coin: false + } ] - assert {:ok, expected} == CryptoCompare.fetch_price_history(3) + assert {:ok, expected} == CryptoCompare.fetch_price_history(3, false) end end end diff --git a/apps/explorer/test/explorer/migrator/address_current_token_balance_token_type_test.exs b/apps/explorer/test/explorer/migrator/address_current_token_balance_token_type_test.exs new file mode 100644 index 000000000000..27591d9c52c1 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/address_current_token_balance_token_type_test.exs @@ -0,0 +1,33 @@ +defmodule Explorer.Migrator.AddressCurrentTokenBalanceTokenTypeTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Migrator.{AddressCurrentTokenBalanceTokenType, MigrationStatus} + alias Explorer.Repo + + describe "Migrate current token balances" do + test "Set token_type for not processed current token balances" do + Enum.each(0..10, fn _x -> + current_token_balance = insert(:address_current_token_balance, token_type: nil) + assert %{token_type: nil} = current_token_balance + end) + + assert MigrationStatus.get_status("ctb_token_type") == nil + + AddressCurrentTokenBalanceTokenType.start_link([]) + Process.sleep(100) + + CurrentTokenBalance + |> Repo.all() + |> Repo.preload(:token) + |> Enum.each(fn ctb -> + assert %{token_type: token_type, token: %{type: token_type}} = ctb + assert not is_nil(token_type) + end) + + assert MigrationStatus.get_status("ctb_token_type") == "completed" + assert BackgroundMigrations.get_ctb_token_type_finished() == true + end + end +end diff --git a/apps/explorer/test/explorer/migrator/address_token_balance_token_type_test.exs b/apps/explorer/test/explorer/migrator/address_token_balance_token_type_test.exs new file mode 100644 index 000000000000..4bf09c57122c --- /dev/null +++ b/apps/explorer/test/explorer/migrator/address_token_balance_token_type_test.exs @@ -0,0 +1,33 @@ +defmodule Explorer.Migrator.AddressTokenBalanceTokenTypeTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.Address.TokenBalance + alias Explorer.Migrator.{AddressTokenBalanceTokenType, MigrationStatus} + alias Explorer.Repo + + describe "Migrate token balances" do + test "Set token_type for not processed token balances" do + Enum.each(0..10, fn _x -> + token_balance = insert(:token_balance, token_type: nil) + assert %{token_type: nil} = token_balance + end) + + assert MigrationStatus.get_status("tb_token_type") == nil + + AddressTokenBalanceTokenType.start_link([]) + Process.sleep(100) + + TokenBalance + |> Repo.all() + |> Repo.preload(:token) + |> Enum.each(fn tb -> + assert %{token_type: token_type, token: %{type: token_type}} = tb + assert not is_nil(token_type) + end) + + assert MigrationStatus.get_status("tb_token_type") == "completed" + assert BackgroundMigrations.get_tb_token_type_finished() == true + end + end +end diff --git a/apps/explorer/test/explorer/migrator/sanitize_incorrect_nft_token_transfers_test.exs b/apps/explorer/test/explorer/migrator/sanitize_incorrect_nft_token_transfers_test.exs new file mode 100644 index 000000000000..cd019e245bd7 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/sanitize_incorrect_nft_token_transfers_test.exs @@ -0,0 +1,63 @@ +defmodule Explorer.Migrator.SanitizeIncorrectNFTTokenTransfersTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.{Block, TokenTransfer} + alias Explorer.Migrator.{SanitizeIncorrectNFTTokenTransfers, MigrationStatus} + alias Explorer.Repo + + describe "Migrate token transfers" do + test "Handles delete and re-fetch" do + %{contract_address: token_address} = insert(:token, type: "ERC-721") + block = insert(:block, consensus: true) + + insert(:token_transfer, + from_address: insert(:address), + block: block, + block_number: block.number, + token_contract_address: token_address, + token_ids: nil + ) + + deposit_log = insert(:log, first_topic: TokenTransfer.weth_deposit_signature()) + + insert(:token_transfer, + from_address: insert(:address), + token_contract_address: token_address, + block: deposit_log.block, + transaction: deposit_log.transaction, + log_index: deposit_log.index + ) + + withdrawal_log = insert(:log, first_topic: TokenTransfer.weth_withdrawal_signature()) + + insert(:token_transfer, + from_address: insert(:address), + token_contract_address: token_address, + block: withdrawal_log.block, + transaction: withdrawal_log.transaction, + log_index: withdrawal_log.index + ) + + erc1155_token = insert(:token, type: "ERC-1155") + + insert(:token_transfer, + from_address: insert(:address), + token_contract_address: erc1155_token.contract_address, + amount: nil, + amounts: nil, + token_ids: nil + ) + + assert MigrationStatus.get_status("sanitize_incorrect_nft") == nil + + SanitizeIncorrectNFTTokenTransfers.start_link([]) + Process.sleep(100) + + assert MigrationStatus.get_status("sanitize_incorrect_nft") == "completed" + + token_address_hash = token_address.hash + assert %{token_contract_address_hash: ^token_address_hash, token_ids: nil} = Repo.one(TokenTransfer) + assert %{consensus: false} = Repo.get_by(Block, hash: block.hash) + end + end +end diff --git a/apps/explorer/test/explorer/migrator/token_transfer_token_type_test.exs b/apps/explorer/test/explorer/migrator/token_transfer_token_type_test.exs new file mode 100644 index 000000000000..b6cc0ee1c372 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/token_transfer_token_type_test.exs @@ -0,0 +1,82 @@ +defmodule Explorer.Migrator.TokenTransferTokenTypeTest do + use Explorer.DataCase, async: false + + import Ecto.Query + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.TokenTransfer + alias Explorer.Migrator.{TokenTransferTokenType, MigrationStatus} + alias Explorer.Repo + + describe "Migrate token transfers" do + test "Set token_type and block_consensus for not processed token transfers" do + %{contract_address_hash: regular_token_hash} = regular_token = insert(:token) + + Enum.each(0..4, fn _x -> + token_transfer = + insert(:token_transfer, + from_address: insert(:address), + token_contract_address: regular_token.contract_address, + token_type: nil, + block_consensus: nil + ) + + assert %{token_type: nil, block_consensus: nil} = token_transfer + end) + + %{contract_address_hash: erc1155_token_hash} = erc1155_token = insert(:token, type: "ERC-1155") + + Enum.each(0..4, fn _x -> + token_transfer = + insert(:token_transfer, + from_address: insert(:address), + token_contract_address: erc1155_token.contract_address, + token_type: nil, + block_consensus: nil, + token_ids: nil + ) + + assert %{token_type: nil, block_consensus: nil, token_ids: nil} = token_transfer + end) + + assert MigrationStatus.get_status("tt_denormalization") == nil + + TokenTransferTokenType.start_link([]) + Process.sleep(100) + + TokenTransfer + |> where([tt], tt.token_contract_address_hash == ^regular_token_hash) + |> Repo.all() + |> Repo.preload([:token, :block]) + |> Enum.each(fn tt -> + assert %{ + token_type: token_type, + token: %{type: token_type}, + block_consensus: consensus, + block: %{consensus: consensus} + } = tt + + assert not is_nil(token_type) + assert not is_nil(consensus) + end) + + TokenTransfer + |> where([tt], tt.token_contract_address_hash == ^erc1155_token_hash) + |> Repo.all() + |> Repo.preload([:token, :block]) + |> Enum.each(fn tt -> + assert %{ + token_type: "ERC-20", + token: %{type: "ERC-1155"}, + block_consensus: consensus, + block: %{consensus: consensus} + } = tt + + assert not is_nil(consensus) + end) + + assert MigrationStatus.get_status("tt_denormalization") == "completed" + assert BackgroundMigrations.get_tt_denormalization_finished() == true + end + end +end diff --git a/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs b/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs new file mode 100644 index 000000000000..e47afe96da4d --- /dev/null +++ b/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs @@ -0,0 +1,43 @@ +defmodule Explorer.Migrator.TransactionsDenormalizationTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.Transaction + alias Explorer.Migrator.{MigrationStatus, TransactionsDenormalization} + alias Explorer.Repo + + describe "Migrate transactions" do + test "Set block_consensus and block_timestamp for not processed transactions" do + Enum.each(0..10, fn _x -> + transaction = + :transaction + |> insert() + |> with_block(block_timestamp: nil, block_consensus: nil) + + assert %{block_consensus: nil, block_timestamp: nil, block: %{consensus: consensus, timestamp: timestamp}} = + transaction + + assert not is_nil(consensus) + assert not is_nil(timestamp) + end) + + assert MigrationStatus.get_status("denormalization") == nil + + TransactionsDenormalization.start_link([]) + Process.sleep(100) + + Transaction + |> Repo.all() + |> Repo.preload(:block) + |> Enum.each(fn t -> + assert %{ + block_consensus: consensus, + block_timestamp: timestamp, + block: %{consensus: consensus, timestamp: timestamp} + } = t + end) + + assert MigrationStatus.get_status("denormalization") == "completed" + end + end +end diff --git a/apps/explorer/test/explorer/smart_contract/helper_test.exs b/apps/explorer/test/explorer/smart_contract/helper_test.exs index 4cd3832434c9..202c0b7edc1f 100644 --- a/apps/explorer/test/explorer/smart_contract/helper_test.exs +++ b/apps/explorer/test/explorer/smart_contract/helper_test.exs @@ -124,4 +124,30 @@ defmodule Explorer.SmartContract.HelperTest do refute Helper.nonpayable?(function) end end + + describe "read_with_wallet_method?" do + test "returns payable method with output in the read tab" do + function = %{ + "type" => "function", + "stateMutability" => "payable", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "returnaddress", + "inputs" => [] + } + + assert Helper.read_with_wallet_method?(function) + end + + test "doesn't return payable method with no output in the read tab" do + function = %{ + "type" => "function", + "stateMutability" => "payable", + "outputs" => [], + "name" => "returnaddress", + "inputs" => [] + } + + refute Helper.read_with_wallet_method?(function) + end + end end diff --git a/apps/explorer/test/explorer/smart_contract/reader_test.exs b/apps/explorer/test/explorer/smart_contract/reader_test.exs index 8eb6e1a2ec6c..4b8a632c5d19 100644 --- a/apps/explorer/test/explorer/smart_contract/reader_test.exs +++ b/apps/explorer/test/explorer/smart_contract/reader_test.exs @@ -102,6 +102,35 @@ defmodule Explorer.SmartContract.ReaderTest do assert %{"6d4ce63c" => {:error, "no function clause matches"}} = response end + + test "with function ABI that is missing the outputs field" do + smart_contract = + build(:smart_contract, + abi: [ + %{ + "type" => "function", + "stateMutability" => "view", + "name" => "assumeLastTokenIdMatches", + "inputs" => [ + %{ + "type" => "uint256", + "name" => "lastTokenId", + "internalType" => "uint256" + } + ] + } + ] + ) + + contract_address_hash = Hash.to_string(smart_contract.address_hash) + abi = smart_contract.abi + + blockchain_get_function_mock() + + response = Reader.query_contract(contract_address_hash, abi, %{"e72878b4" => [123]}, false) + + assert response == %{"e72878b4" => {:ok, []}} + end end describe "query_verified_contract/3" do diff --git a/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs b/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs index a9255d04c796..d1d48cc65452 100644 --- a/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs @@ -1,4 +1,4 @@ -defmodule Explorer.Token.MetadataRetrieverTest do +defmodule Explorer.Token.InstanceMetadataRetrieverTest do use EthereumJSONRPC.Case alias Indexer.Fetcher.TokenInstance.MetadataRetriever @@ -342,7 +342,7 @@ defmodule Explorer.Token.MetadataRetrieverTest do {:error, "(3) execution reverted: Nonexistent token (0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000114e6f6e6578697374656e7420746f6b656e000000000000000000000000000000)"} - assert {:ok, %{error: "VM execution error"}} == MetadataRetriever.fetch_json(data) + assert {:error, "VM execution error"} == MetadataRetriever.fetch_json(data) end test "Process CIDv0 IPFS links" do diff --git a/apps/explorer/test/explorer/token_transfer_token_id_migration/lowest_block_number_updater_test.exs b/apps/explorer/test/explorer/token_transfer_token_id_migration/lowest_block_number_updater_test.exs deleted file mode 100644 index bdd94920db2c..000000000000 --- a/apps/explorer/test/explorer/token_transfer_token_id_migration/lowest_block_number_updater_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Explorer.TokenTransferTokenIdMigration.LowestBlockNumberUpdaterTest do - use Explorer.DataCase, async: false - - alias Explorer.Repo - alias Explorer.TokenTransferTokenIdMigration.LowestBlockNumberUpdater - alias Explorer.Utility.TokenTransferTokenIdMigratorProgress - - describe "Add range and update last processed block number" do - test "add_range/2" do - TokenTransferTokenIdMigratorProgress.update_last_processed_block_number(2000, true) - LowestBlockNumberUpdater.start_link([]) - - LowestBlockNumberUpdater.add_range(1000, 500) - LowestBlockNumberUpdater.add_range(1500, 1001) - Process.sleep(10) - - assert %{last_processed_block_number: 2000, processed_ranges: [1500..500//-1]} = - :sys.get_state(LowestBlockNumberUpdater) - - assert %{last_processed_block_number: 2000} = Repo.one(TokenTransferTokenIdMigratorProgress) - - LowestBlockNumberUpdater.add_range(499, 300) - LowestBlockNumberUpdater.add_range(299, 0) - Process.sleep(10) - - assert %{last_processed_block_number: 2000, processed_ranges: [1500..0//-1]} = - :sys.get_state(LowestBlockNumberUpdater) - - assert %{last_processed_block_number: 2000} = Repo.one(TokenTransferTokenIdMigratorProgress) - - LowestBlockNumberUpdater.add_range(1999, 1501) - Process.sleep(10) - assert %{last_processed_block_number: 0, processed_ranges: []} = :sys.get_state(LowestBlockNumberUpdater) - assert %{last_processed_block_number: 0} = Repo.one(TokenTransferTokenIdMigratorProgress) - end - end -end diff --git a/apps/explorer/test/explorer/token_transfer_token_id_migration/worker_test.exs b/apps/explorer/test/explorer/token_transfer_token_id_migration/worker_test.exs deleted file mode 100644 index 8797e90130e6..000000000000 --- a/apps/explorer/test/explorer/token_transfer_token_id_migration/worker_test.exs +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Explorer.TokenTransferTokenIdMigration.WorkerTest do - use Explorer.DataCase, async: false - - alias Explorer.Repo - alias Explorer.TokenTransferTokenIdMigration.{LowestBlockNumberUpdater, Worker} - alias Explorer.Utility.TokenTransferTokenIdMigratorProgress - - describe "Move TokenTransfer token_id to token_ids" do - test "Move token_ids and update last processed block number" do - insert(:token_transfer, block_number: 1, token_id: 1, transaction: insert(:transaction)) - insert(:token_transfer, block_number: 500, token_id: 2, transaction: insert(:transaction)) - insert(:token_transfer, block_number: 1000, token_id: 3, transaction: insert(:transaction)) - insert(:token_transfer, block_number: 1500, token_id: 4, transaction: insert(:transaction)) - insert(:token_transfer, block_number: 2000, token_id: 5, transaction: insert(:transaction)) - - TokenTransferTokenIdMigratorProgress.update_last_processed_block_number(3000, true) - LowestBlockNumberUpdater.start_link([]) - - Worker.start_link(idx: 1, first_block: 0, last_block: 3000, step: 2) - Worker.start_link(idx: 2, first_block: 0, last_block: 3000, step: 2) - Worker.start_link(idx: 3, first_block: 0, last_block: 3000, step: 2) - Process.sleep(200) - - token_transfers = Repo.all(Explorer.Chain.TokenTransfer) - assert Enum.all?(token_transfers, fn tt -> is_nil(tt.token_id) end) - - expected_token_ids = [[Decimal.new(1)], [Decimal.new(2)], [Decimal.new(3)], [Decimal.new(4)], [Decimal.new(5)]] - assert ^expected_token_ids = token_transfers |> Enum.map(& &1.token_ids) |> Enum.sort_by(&List.first/1) - end - end -end diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index f93e1bcf7aa7..da18760983cc 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -35,18 +35,10 @@ defmodule Explorer.DataCase do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account) - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.PolygonEdge) - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.PolygonZkevm) - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.RSK) - :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Suave) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()}) - Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonEdge, {:shared, self()}) - Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonZkevm, {:shared, self()}) - Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, {:shared, self()}) - Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, {:shared, self()}) end Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 33c9f6347796..9953bf1a59ba 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -21,7 +21,9 @@ defmodule Explorer.Factory do } alias Explorer.Admin.Administrator + alias Explorer.Chain.Beacon.{Blob, BlobTransaction} alias Explorer.Chain.Block.{EmissionReward, Range, Reward} + alias Explorer.Chain.Stability.Validator, as: ValidatorStability alias Explorer.Chain.{ Address, @@ -38,6 +40,7 @@ defmodule Explorer.Factory do Log, PendingBlockOperation, SmartContract, + SmartContractAdditionalSource, Token, TokenTransfer, Token.Instance, @@ -45,6 +48,8 @@ defmodule Explorer.Factory do Withdrawal } + alias Explorer.Chain.Optimism.OutputRoot + alias Explorer.SmartContract.Helper alias Explorer.Tags.{AddressTag, AddressToTag} alias Explorer.Market.MarketHistory @@ -101,6 +106,10 @@ defmodule Explorer.Factory do "ERC-721" => %{ "incoming" => random_bool(), "outcoming" => random_bool() + }, + "ERC-404" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() } }, "notification_methods" => %{ @@ -125,6 +134,8 @@ defmodule Explorer.Factory do watch_erc_721_output: random_bool(), watch_erc_1155_input: random_bool(), watch_erc_1155_output: random_bool(), + watch_erc_404_input: random_bool(), + watch_erc_404_output: random_bool(), notify_email: random_bool() } end @@ -200,6 +211,8 @@ defmodule Explorer.Factory do watch_erc_721_output: random_bool(), watch_erc_1155_input: random_bool(), watch_erc_1155_output: random_bool(), + watch_erc_404_input: random_bool(), + watch_erc_404_output: random_bool(), notify_email: random_bool() } end @@ -533,7 +546,7 @@ defmodule Explorer.Factory do %Transaction{index: nil} = transaction, # The `transaction.block` must be consensus. Non-consensus blocks can only be associated with the # `transaction_forks`. - %Block{consensus: true, hash: block_hash, number: block_number}, + %Block{consensus: true, hash: block_hash, number: block_number, timestamp: timestamp}, collated_params ) when is_list(collated_params) do @@ -542,6 +555,8 @@ defmodule Explorer.Factory do cumulative_gas_used = collated_params[:cumulative_gas_used] || Enum.random(21_000..100_000) gas_used = collated_params[:gas_used] || Enum.random(21_000..100_000) status = Keyword.get(collated_params, :status, Enum.random([:ok, :error])) + block_timestamp = Keyword.get(collated_params, :block_timestamp, timestamp) + block_consensus = Keyword.get(collated_params, :block_consensus, true) error = (status == :error && collated_params[:error]) || nil @@ -555,7 +570,9 @@ defmodule Explorer.Factory do error: error, gas_used: gas_used, index: next_transaction_index, - status: status + status: status, + block_timestamp: block_timestamp, + block_consensus: block_consensus }) |> Repo.update!() |> Repo.preload(:block) @@ -675,8 +692,7 @@ defmodule Explorer.Factory do index: sequence("log_index", & &1), second_topic: nil, third_topic: nil, - transaction: build(:transaction), - type: sequence("0x") + transaction: build(:transaction) } end @@ -743,7 +759,7 @@ defmodule Explorer.Factory do contract_code = Map.fetch!(contract_code_info(), :bytecode) token_address = insert(:contract_address, contract_code: contract_code) - insert(:token, contract_address: token_address) + token = insert(:token, contract_address: token_address) %TokenTransfer{ block: build(:block), @@ -752,8 +768,10 @@ defmodule Explorer.Factory do from_address: from_address, to_address: to_address, token_contract_address: token_address, + token_type: token.type, transaction: log.transaction, - log_index: log.index + log_index: log.index, + block_consensus: true } end @@ -807,7 +825,8 @@ defmodule Explorer.Factory do s: sequence(:transaction_s, & &1), to_address: build(:address), v: Enum.random(27..30), - value: Enum.random(1..100_000) + value: Enum.random(1..100_000), + block_timestamp: DateTime.utc_now() } end @@ -870,6 +889,10 @@ defmodule Explorer.Factory do } end + def smart_contract_additional_source_factory do + %SmartContractAdditionalSource{} + end + def unique_smart_contract_factory do Map.replace(smart_contract_factory(), :name, sequence("SimpleStorage")) end @@ -888,8 +911,14 @@ defmodule Explorer.Factory do %Instance{ token_contract_address_hash: insert(:token).contract_address_hash, token_id: sequence("token_id", & &1), - metadata: %{key: "value"}, - error: nil + metadata: %{ + "key" => sequence("value"), + "image_url" => sequence("image_url"), + "animation_url" => sequence("image_url"), + "external_url" => sequence("external_url") + }, + error: nil, + owner_address_hash: insert(:address).hash } end @@ -930,7 +959,14 @@ defmodule Explorer.Factory do end def address_current_token_balance_with_token_id_factory do - {token_type, token_id} = Enum.random([{"ERC-20", nil}, {"ERC-721", nil}, {"ERC-1155", Enum.random(1..100_000)}]) + {token_type, token_id} = + Enum.random([ + {"ERC-20", nil}, + {"ERC-721", nil}, + {"ERC-1155", Enum.random(1..100_000)}, + {"ERC-404", nil}, + {"ERC-404", Enum.random(1..100_000)} + ]) %CurrentTokenBalance{ address: build(:address), @@ -943,6 +979,41 @@ defmodule Explorer.Factory do } end + def address_current_token_balance_with_token_id_and_fixed_token_type_factory(%{ + token_type: token_type, + address: address, + token_id: token_id, + token_contract_address_hash: token_contract_address_hash, + value: value + }) do + %CurrentTokenBalance{ + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number(), + value: value, + value_fetched_at: DateTime.utc_now(), + token_id: token_id, + token_type: token_type + } + end + + def address_current_token_balance_with_token_id_and_fixed_token_type_factory(%{ + token_type: token_type, + address: address, + token_id: token_id, + token_contract_address_hash: token_contract_address_hash + }) do + %CurrentTokenBalance{ + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number(), + value: Enum.random(1_000_000_000_000_000_000..10_000_000_000_000_000_000), + value_fetched_at: DateTime.utc_now(), + token_id: token_id, + token_type: token_type + } + end + def address_current_token_balance_with_token_id_and_fixed_token_type_factory(%{ token_type: token_type, address: address, @@ -1041,5 +1112,63 @@ defmodule Explorer.Factory do sequence("withdrawal_validator_index", & &1) end + def blob_factory do + kzg_commitment = data(:kzg_commitment) + + %Blob{ + hash: Blob.hash(kzg_commitment.bytes), + blob_data: data(:blob_data), + kzg_commitment: kzg_commitment, + kzg_proof: data(:kzg_proof) + } + end + + def blob_transaction_factory do + %BlobTransaction{ + hash: insert(:transaction) |> with_block() |> Map.get(:hash), + max_fee_per_blob_gas: Decimal.new(1_000_000_000), + blob_gas_price: Decimal.new(1_000_000_000), + blob_gas_used: Decimal.new(131_072), + blob_versioned_hashes: [] + } + end + + def op_output_root_factory do + %OutputRoot{ + l2_output_index: op_output_root_l2_output_index(), + l2_block_number: insert(:block) |> Map.get(:number), + l1_transaction_hash: transaction_hash(), + l1_timestamp: DateTime.utc_now(), + l1_block_number: op_output_root_l1_block_number(), + output_root: op_output_root_hash() + } + end + + defp op_output_root_l2_output_index do + sequence("op_output_root_l2_output_index", & &1) + end + + defp op_output_root_l1_block_number do + sequence("op_output_root_l1_block_number", & &1) + end + + defp op_output_root_hash do + {:ok, hash} = + "op_output_root_hash" + |> sequence(& &1) + |> Hash.Full.cast() + + hash + end + def random_bool, do: Enum.random([true, false]) + + def validator_stability_factory do + address = insert(:address) + + %ValidatorStability{ + address_hash: address.hash, + state: Enum.random(0..2) + } + end end diff --git a/apps/explorer/test/support/fakes/no_op_price_source.ex b/apps/explorer/test/support/fakes/no_op_price_source.ex index b9a460ac8876..fa896d90032a 100644 --- a/apps/explorer/test/support/fakes/no_op_price_source.ex +++ b/apps/explorer/test/support/fakes/no_op_price_source.ex @@ -6,7 +6,7 @@ defmodule Explorer.ExchangeRates.Source.NoOpPriceSource do @behaviour SourcePrice @impl SourcePrice - def fetch_price_history(_previous_days) do + def fetch_price_history(_previous_days, _secondary_coin?) do {:ok, []} end end diff --git a/apps/explorer/test/support/fakes/one_coin_source.ex b/apps/explorer/test/support/fakes/one_coin_source.ex index 4301f24635f1..ee38c6f09ba4 100644 --- a/apps/explorer/test/support/fakes/one_coin_source.ex +++ b/apps/explorer/test/support/fakes/one_coin_source.ex @@ -19,7 +19,8 @@ defmodule Explorer.ExchangeRates.Source.OneCoinSource do tvl_usd: Decimal.new(100_500_000), symbol: Explorer.coin(), usd_value: Decimal.new(1), - volume_24h_usd: Decimal.new(1) + volume_24h_usd: Decimal.new(1), + image_url: nil } [pseudo_token] diff --git a/apps/explorer/test/support/fixture/chain_spec/zkatana_genesis.json b/apps/explorer/test/support/fixture/chain_spec/zkatana_genesis.json new file mode 100644 index 000000000000..7a7ca0523658 --- /dev/null +++ b/apps/explorer/test/support/fixture/chain_spec/zkatana_genesis.json @@ -0,0 +1,102 @@ +{ + "root": "0xd8efe6b2ede4af8771fa2ab26186b292fd76c359d9307a90c5972e22d5be6676", + "genesis": [ + { + "contractName": "PolygonZkEVMDeployer", + "balance": "0", + "nonce": "4", + "address": "0xd03c1a9c2fe2C6f3927C398A90272FE95bD3CDaF", + "bytecode": "0x60806040526004361061006e575f3560e01c8063715018a61161004c578063715018a6146100e25780638da5cb5b146100f6578063e11ae6cb1461011f578063f2fde38b14610132575f80fd5b80632b79805a146100725780634a94d487146100875780636d07dbf81461009a575b5f80fd5b610085610080366004610908565b610151565b005b6100856100953660046109a2565b6101c2565b3480156100a5575f80fd5b506100b96100b43660046109f5565b610203565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b3480156100ed575f80fd5b50610085610215565b348015610101575f80fd5b505f5473ffffffffffffffffffffffffffffffffffffffff166100b9565b61008561012d366004610a15565b610228565b34801561013d575f80fd5b5061008561014c366004610a61565b61028e565b61015961034a565b5f6101658585856103ca565b90506101718183610527565b5060405173ffffffffffffffffffffffffffffffffffffffff821681527fba82f25fed02cd2a23d9f5d11c2ef588d22af5437cbf23bfe61d87257c480e4c9060200160405180910390a15050505050565b6101ca61034a565b6101d583838361056a565b506040517f25adb19089b6a549831a273acdf7908cff8b7ee5f551f8d1d37996cf01c5df5b905f90a1505050565b5f61020e8383610598565b9392505050565b61021d61034a565b6102265f6105a4565b565b61023061034a565b5f61023c8484846103ca565b60405173ffffffffffffffffffffffffffffffffffffffff821681529091507fba82f25fed02cd2a23d9f5d11c2ef588d22af5437cbf23bfe61d87257c480e4c9060200160405180910390a150505050565b61029661034a565b73ffffffffffffffffffffffffffffffffffffffff811661033e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b610347816105a4565b50565b5f5473ffffffffffffffffffffffffffffffffffffffff163314610226576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610335565b5f83471015610435576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f437265617465323a20696e73756666696369656e742062616c616e63650000006044820152606401610335565b81515f0361049f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f437265617465323a2062797465636f6465206c656e677468206973207a65726f6044820152606401610335565b8282516020840186f5905073ffffffffffffffffffffffffffffffffffffffff811661020e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601960248201527f437265617465323a204661696c6564206f6e206465706c6f79000000000000006044820152606401610335565b606061020e83835f6040518060400160405280601e81526020017f416464726573733a206c6f772d6c6576656c2063616c6c206661696c65640000815250610618565b6060610590848484604051806060016040528060298152602001610b0860299139610618565b949350505050565b5f61020e83833061072d565b5f805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6060824710156106aa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f416464726573733a20696e73756666696369656e742062616c616e636520666f60448201527f722063616c6c00000000000000000000000000000000000000000000000000006064820152608401610335565b5f808673ffffffffffffffffffffffffffffffffffffffff1685876040516106d29190610a9c565b5f6040518083038185875af1925050503d805f811461070c576040519150601f19603f3d011682016040523d82523d5f602084013e610711565b606091505b509150915061072287838387610756565b979650505050505050565b5f604051836040820152846020820152828152600b8101905060ff815360559020949350505050565b606083156107eb5782515f036107e45773ffffffffffffffffffffffffffffffffffffffff85163b6107e4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610335565b5081610590565b61059083838151156108005781518083602001fd5b806040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103359190610ab7565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f82601f830112610870575f80fd5b813567ffffffffffffffff8082111561088b5761088b610834565b604051601f83017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f011681019082821181831017156108d1576108d1610834565b816040528381528660208588010111156108e9575f80fd5b836020870160208301375f602085830101528094505050505092915050565b5f805f806080858703121561091b575f80fd5b8435935060208501359250604085013567ffffffffffffffff80821115610940575f80fd5b61094c88838901610861565b93506060870135915080821115610961575f80fd5b5061096e87828801610861565b91505092959194509250565b803573ffffffffffffffffffffffffffffffffffffffff8116811461099d575f80fd5b919050565b5f805f606084860312156109b4575f80fd5b6109bd8461097a565b9250602084013567ffffffffffffffff8111156109d8575f80fd5b6109e486828701610861565b925050604084013590509250925092565b5f8060408385031215610a06575f80fd5b50508035926020909101359150565b5f805f60608486031215610a27575f80fd5b8335925060208401359150604084013567ffffffffffffffff811115610a4b575f80fd5b610a5786828701610861565b9150509250925092565b5f60208284031215610a71575f80fd5b61020e8261097a565b5f5b83811015610a94578181015183820152602001610a7c565b50505f910152565b5f8251610aad818460208701610a7a565b9190910192915050565b602081525f8251806020840152610ad5816040850160208701610a7a565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2063616c6c20776974682076616c7565206661696c6564a2646970667358221220330b94dc698c4d290bf55c23f13b473cde6a6bae0030cb902de18af54e35839f64736f6c63430008140033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x00000000000000000000000056b9eaf5d19639acc16c6373c66e5a1f61cf29b6" + } + }, + { + "contractName": "ProxyAdmin", + "balance": "0", + "nonce": "1", + "address": "0xfE3306Bb4124E90eA08Af2776008592052Ecb9e0", + "bytecode": "0x608060405260043610610079575f3560e01c80639623609d1161004c5780639623609d1461012357806399a88ec414610136578063f2fde38b14610155578063f3b7dead14610174575f80fd5b8063204e1c7a1461007d578063715018a6146100c55780637eff275e146100db5780638da5cb5b146100fa575b5f80fd5b348015610088575f80fd5b5061009c6100973660046105e8565b610193565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b3480156100d0575f80fd5b506100d9610244565b005b3480156100e6575f80fd5b506100d96100f536600461060a565b610257565b348015610105575f80fd5b505f5473ffffffffffffffffffffffffffffffffffffffff1661009c565b6100d961013136600461066e565b6102e0565b348015610141575f80fd5b506100d961015036600461060a565b610371565b348015610160575f80fd5b506100d961016f3660046105e8565b6103cd565b34801561017f575f80fd5b5061009c61018e3660046105e8565b610489565b5f805f8373ffffffffffffffffffffffffffffffffffffffff166040516101dd907f5c60da1b00000000000000000000000000000000000000000000000000000000815260040190565b5f60405180830381855afa9150503d805f8114610215576040519150601f19603f3d011682016040523d82523d5f602084013e61021a565b606091505b509150915081610228575f80fd5b8080602001905181019061023c919061075b565b949350505050565b61024c6104d3565b6102555f610553565b565b61025f6104d3565b6040517f8f28397000000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8281166004830152831690638f283970906024015b5f604051808303815f87803b1580156102c6575f80fd5b505af11580156102d8573d5f803e3d5ffd5b505050505050565b6102e86104d3565b6040517f4f1ef28600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff841690634f1ef28690349061033e9086908690600401610776565b5f604051808303818588803b158015610355575f80fd5b505af1158015610367573d5f803e3d5ffd5b5050505050505050565b6103796104d3565b6040517f3659cfe600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8281166004830152831690633659cfe6906024016102af565b6103d56104d3565b73ffffffffffffffffffffffffffffffffffffffff811661047d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f646472657373000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b61048681610553565b50565b5f805f8373ffffffffffffffffffffffffffffffffffffffff166040516101dd907ff851a44000000000000000000000000000000000000000000000000000000000815260040190565b5f5473ffffffffffffffffffffffffffffffffffffffff163314610255576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610474565b5f805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b73ffffffffffffffffffffffffffffffffffffffff81168114610486575f80fd5b5f602082840312156105f8575f80fd5b8135610603816105c7565b9392505050565b5f806040838503121561061b575f80fd5b8235610626816105c7565b91506020830135610636816105c7565b809150509250929050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f805f60608486031215610680575f80fd5b833561068b816105c7565b9250602084013561069b816105c7565b9150604084013567ffffffffffffffff808211156106b7575f80fd5b818601915086601f8301126106ca575f80fd5b8135818111156106dc576106dc610641565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f0116810190838211818310171561072257610722610641565b8160405282815289602084870101111561073a575f80fd5b826020860160208301375f6020848301015280955050505050509250925092565b5f6020828403121561076b575f80fd5b8151610603816105c7565b73ffffffffffffffffffffffffffffffffffffffff831681525f602060408184015283518060408501525f5b818110156107be578581018301518582016060015282016107a2565b505f6060828601015260607fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010192505050939250505056fea26469706673582212203083a4ccc2e42eed60bd19037f2efa77ed086dc7a5403f75bebb995dcba2221c64736f6c63430008140033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000007d38458ed6b9b04a999e86057143c5faa0d259e9" + } + }, + { + "contractName": "PolygonZkEVMBridge implementation", + "balance": "0", + "nonce": "1", + "address": "0xeaE46b49f2FC2D5aDfC457a19d6E34aEc6Eb6C3A", + "bytecode": "0x60806040526004361062000197575f3560e01c8063647c576c11620000e2578063be5831c71162000086578063dbc16976116200005e578063dbc16976146200061a578063ee25560b1462000631578063fb5708341462000660575f80fd5b8063be5831c71462000591578063cd58657914620005cc578063d02103ca14620005e3575f80fd5b80639e34070f11620000ba5780639e34070f14620004f1578063aaa13cc21462000534578063bab161bf1462000558575f80fd5b8063647c576c146200047157806379e2cf97146200049557806381b1c17414620004ac575f80fd5b80632d2c9d94116200014a57806334ac9cf2116200012257806334ac9cf2146200033a5780633ae0504714620003685780633e197043146200037f575f80fd5b80632d2c9d9414620002695780632dfdf0b5146200028d578063318aee3d14620002b3575f80fd5b806322e95f2c116200017e57806322e95f2c14620001e4578063240ff378146200022e5780632cffd02e1462000245575f80fd5b806315064c96146200019b5780632072f6c514620001cb575b5f80fd5b348015620001a7575f80fd5b50606854620001b69060ff1681565b60405190151581526020015b60405180910390f35b348015620001d7575f80fd5b50620001e262000684565b005b348015620001f0575f80fd5b5062000208620002023660046200323f565b620006e2565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001620001c2565b620001e26200023f366004620032cf565b62000784565b34801562000251575f80fd5b50620001e26200026336600462003360565b620009ab565b34801562000275575f80fd5b50620001e26200028736600462003360565b62000f30565b34801562000299575f80fd5b50620002a460535481565b604051908152602001620001c2565b348015620002bf575f80fd5b5062000308620002d13660046200343e565b606b6020525f908152604090205463ffffffff811690640100000000900473ffffffffffffffffffffffffffffffffffffffff1682565b6040805163ffffffff909316835273ffffffffffffffffffffffffffffffffffffffff909116602083015201620001c2565b34801562000346575f80fd5b50606c54620002089073ffffffffffffffffffffffffffffffffffffffff1681565b34801562000374575f80fd5b50620002a462001130565b3480156200038b575f80fd5b50620002a46200039d36600462003472565b6040517fff0000000000000000000000000000000000000000000000000000000000000060f889901b1660208201527fffffffff0000000000000000000000000000000000000000000000000000000060e088811b821660218401527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606089811b821660258601529188901b909216603984015285901b16603d82015260518101839052607181018290525f90609101604051602081830303815290604052805190602001209050979650505050505050565b3480156200047d575f80fd5b50620001e26200048f366004620034f7565b62001215565b348015620004a1575f80fd5b50620001e26200145e565b348015620004b8575f80fd5b5062000208620004ca36600462003544565b606a6020525f908152604090205473ffffffffffffffffffffffffffffffffffffffff1681565b348015620004fd575f80fd5b50620001b66200050f36600462003544565b600881901c5f90815260696020526040902054600160ff9092169190911b9081161490565b34801562000540575f80fd5b5062000208620005523660046200355c565b62001498565b34801562000564575f80fd5b506068546200057b90610100900463ffffffff1681565b60405163ffffffff9091168152602001620001c2565b3480156200059d575f80fd5b506068546200057b90790100000000000000000000000000000000000000000000000000900463ffffffff1681565b620001e2620005dd36600462003609565b62001682565b348015620005ef575f80fd5b50606854620002089065010000000000900473ffffffffffffffffffffffffffffffffffffffff1681565b34801562000626575f80fd5b50620001e262001bd4565b3480156200063d575f80fd5b50620002a46200064f36600462003544565b60696020525f908152604090205481565b3480156200066c575f80fd5b50620001b66200067e366004620036a5565b62001c30565b606c5473ffffffffffffffffffffffffffffffffffffffff163314620006d6576040517fe2e8106b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b620006e062001d18565b565b6040805160e084901b7fffffffff0000000000000000000000000000000000000000000000000000000016602080830191909152606084901b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016602483015282516018818403018152603890920183528151918101919091205f908152606a909152205473ffffffffffffffffffffffffffffffffffffffff165b92915050565b60685460ff1615620007c2576040517f2f0047fc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60685463ffffffff868116610100909204161480620007e85750600263ffffffff861610155b1562000820576040517f0595ea2e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7f501781209a1f8899323b96b4ef08b168df93e0a90c673d1e4cce39366cb62f9b6001606860019054906101000a900463ffffffff16338888348888605354604051620008769998979695949392919062003736565b60405180910390a1620009936200098d6001606860019054906101000a900463ffffffff16338989348989604051620008b1929190620037b0565b60405180910390206040517fff0000000000000000000000000000000000000000000000000000000000000060f889901b1660208201527fffffffff0000000000000000000000000000000000000000000000000000000060e088811b821660218401527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606089811b821660258601529188901b909216603984015285901b16603d82015260518101839052607181018290525f90609101604051602081830303815290604052805190602001209050979650505050505050565b62001dab565b8215620009a457620009a462001ebf565b5050505050565b60685460ff1615620009e9576040517f2f0047fc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b620009ff8b8b8b8b8b8b8b8b8b8b8b5f62001f8f565b73ffffffffffffffffffffffffffffffffffffffff861662000ad757604080515f8082526020820190925273ffffffffffffffffffffffffffffffffffffffff861690859060405162000a53919062003810565b5f6040518083038185875af1925050503d805f811462000a8f576040519150601f19603f3d011682016040523d82523d5f602084013e62000a94565b606091505b505090508062000ad0576040517f6747a28800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5062000eb8565b60685463ffffffff61010090910481169088160362000b195762000b1373ffffffffffffffffffffffffffffffffffffffff871685856200217a565b62000eb8565b6040517fffffffff0000000000000000000000000000000000000000000000000000000060e089901b1660208201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606088901b1660248201525f90603801604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815291815281516020928301205f818152606a90935291205490915073ffffffffffffffffffffffffffffffffffffffff168062000e2f575f808062000beb868801886200391f565b9250925092505f8584848460405162000c0490620031f8565b62000c1293929190620039db565b8190604051809103905ff590508015801562000c30573d5f803e3d5ffd5b506040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8c81166004830152602482018c9052919250908216906340c10f19906044015f604051808303815f87803b15801562000ca3575f80fd5b505af115801562000cb6573d5f803e3d5ffd5b5050505080606a5f8881526020019081526020015f205f6101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060405180604001604052808e63ffffffff1681526020018d73ffffffffffffffffffffffffffffffffffffffff16815250606b5f8373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f820151815f015f6101000a81548163ffffffff021916908363ffffffff1602179055506020820151815f0160046101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050507f490e59a1701b938786ac72570a1efeac994a3dbe96e2e883e19e902ace6e6a398d8d838b8b60405162000e1d95949392919062003a17565b60405180910390a15050505062000eb5565b6040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8781166004830152602482018790528216906340c10f19906044015f604051808303815f87803b15801562000e9d575f80fd5b505af115801562000eb0573d5f803e3d5ffd5b505050505b50505b6040805163ffffffff8c811682528916602082015273ffffffffffffffffffffffffffffffffffffffff88811682840152861660608201526080810185905290517f25308c93ceeed162da955b3f7ce3e3f93606579e40fb92029faa9efe275459839181900360a00190a15050505050505050505050565b60685460ff161562000f6e576040517f2f0047fc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b62000f858b8b8b8b8b8b8b8b8b8b8b600162001f8f565b5f8473ffffffffffffffffffffffffffffffffffffffff1684888a868660405160240162000fb7949392919062003a5e565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f1806b5f200000000000000000000000000000000000000000000000000000000179052516200103a919062003810565b5f6040518083038185875af1925050503d805f811462001076576040519150601f19603f3d011682016040523d82523d5f602084013e6200107b565b606091505b5050905080620010b7576040517f37e391c300000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040805163ffffffff8d811682528a16602082015273ffffffffffffffffffffffffffffffffffffffff89811682840152871660608201526080810186905290517f25308c93ceeed162da955b3f7ce3e3f93606579e40fb92029faa9efe275459839181900360a00190a1505050505050505050505050565b6053545f90819081805b60208110156200120c578083901c6001166001036200119d576033816020811062001169576200116962003aa5565b01546040805160208101929092528101859052606001604051602081830303815290604052805190602001209350620011ca565b60408051602081018690529081018390526060016040516020818303038152906040528051906020012093505b60408051602081018490529081018390526060016040516020818303038152906040528051906020012091508080620012039062003aff565b9150506200113a565b50919392505050565b5f54610100900460ff16158080156200123457505f54600160ff909116105b806200124f5750303b1580156200124f57505f5460ff166001145b620012e1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a656400000000000000000000000000000000000060648201526084015b60405180910390fd5b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905580156200133e575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b606880547fffffffffffffff000000000000000000000000000000000000000000000000ff1661010063ffffffff8716027fffffffffffffff0000000000000000000000000000000000000000ffffffffff16176501000000000073ffffffffffffffffffffffffffffffffffffffff8681169190910291909117909155606c80547fffffffffffffffffffffffff000000000000000000000000000000000000000016918416919091179055620013f562002250565b801562001458575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b50505050565b605354606854790100000000000000000000000000000000000000000000000000900463ffffffff161015620006e057620006e062001ebf565b6040517fffffffff0000000000000000000000000000000000000000000000000000000060e089901b1660208201527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606088901b1660248201525f9081906038016040516020818303038152906040528051906020012090505f60ff60f81b3083604051806020016200152c90620031f8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082820381018352601f90910116604081905262001577908d908d908d908d908d9060200162003b39565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815290829052620015b5929160200162003b79565b604051602081830303815290604052805190602001206040516020016200163e94939291907fff0000000000000000000000000000000000000000000000000000000000000094909416845260609290921b7fffffffffffffffffffffffffffffffffffffffff0000000000000000000000001660018401526015830152603582015260550190565b604080518083037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001815291905280516020909101209a9950505050505050505050565b60685460ff1615620016c0576040517f2f0047fc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b620016ca620022f2565b60685463ffffffff888116610100909204161480620016f05750600263ffffffff881610155b1562001728576040517f0595ea2e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f8060608773ffffffffffffffffffffffffffffffffffffffff88166200178c5788341462001783576040517fb89240f500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f925062001a79565b3415620017c5576040517f798ee6f100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8089165f908152606b602090815260409182902082518084019093525463ffffffff8116835264010000000090049092169181018290529015620018ae576040517f9dc29fac000000000000000000000000000000000000000000000000000000008152336004820152602481018b905273ffffffffffffffffffffffffffffffffffffffff8a1690639dc29fac906044015f604051808303815f87803b15801562001884575f80fd5b505af115801562001897573d5f803e3d5ffd5b5050505080602001519450805f0151935062001a77565b8515620018c357620018c3898b898962002367565b6040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201525f9073ffffffffffffffffffffffffffffffffffffffff8b16906370a0823190602401602060405180830381865afa1580156200192e573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019062001954919062003bab565b90506200197a73ffffffffffffffffffffffffffffffffffffffff8b1633308e6200287a565b6040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201525f9073ffffffffffffffffffffffffffffffffffffffff8c16906370a0823190602401602060405180830381865afa158015620019e5573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019062001a0b919062003bab565b905062001a19828262003bc3565b6068548c9850610100900463ffffffff169650935062001a3987620028da565b62001a448c620029ee565b62001a4f8d62002af7565b60405160200162001a6393929190620039db565b604051602081830303815290604052945050505b505b7f501781209a1f8899323b96b4ef08b168df93e0a90c673d1e4cce39366cb62f9b5f84868e8e868860535460405162001aba98979695949392919062003bd9565b60405180910390a162001bac6200098d5f85878f8f8789805190602001206040517fff0000000000000000000000000000000000000000000000000000000000000060f889901b1660208201527fffffffff0000000000000000000000000000000000000000000000000000000060e088811b821660218401527fffffffffffffffffffffffffffffffffffffffff000000000000000000000000606089811b821660258601529188901b909216603984015285901b16603d82015260518101839052607181018290525f90609101604051602081830303815290604052805190602001209050979650505050505050565b861562001bbd5762001bbd62001ebf565b5050505062001bcb60018055565b50505050505050565b606c5473ffffffffffffffffffffffffffffffffffffffff16331462001c26576040517fe2e8106b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b620006e062002bf5565b5f84815b602081101562001d0a57600163ffffffff8616821c8116900362001ca65785816020811062001c675762001c6762003aa5565b60200201358260405160200162001c88929190918252602082015260400190565b60405160208183030381529060405280519060200120915062001cf5565b8186826020811062001cbc5762001cbc62003aa5565b602002013560405160200162001cdc929190918252602082015260400190565b6040516020818303038152906040528051906020012091505b8062001d018162003aff565b91505062001c34565b50821490505b949350505050565b60685460ff161562001d56576040517f2f0047fc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b606880547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790556040517f2261efe5aef6fedc1fd1550b25facc9181745623049c7901287030b9ad1a5497905f90a1565b80600162001dbc6020600262003d88565b62001dc8919062003bc3565b6053541062001e03576040517fef5ccf6600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f60535f815462001e149062003aff565b918290555090505f5b602081101562001eaf578082901c60011660010362001e5557826033826020811062001e4d5762001e4d62003aa5565b015550505050565b6033816020811062001e6b5762001e6b62003aa5565b01546040805160208101929092528101849052606001604051602081830303815290604052805190602001209250808062001ea69062003aff565b91505062001e1d565b5062001eba62003d95565b505050565b6053546068805463ffffffff909216790100000000000000000000000000000000000000000000000000027fffffff00000000ffffffffffffffffffffffffffffffffffffffffffffffffff909216919091179081905573ffffffffffffffffffffffffffffffffffffffff65010000000000909104166333d6247d62001f4562001130565b6040518263ffffffff1660e01b815260040162001f6491815260200190565b5f604051808303815f87803b15801562001f7c575f80fd5b505af115801562001458573d5f803e3d5ffd5b62001fa08b63ffffffff1662002c84565b6068546040805160208082018e90528183018d9052825180830384018152606083019384905280519101207f257b36320000000000000000000000000000000000000000000000000000000090925260648101919091525f9165010000000000900473ffffffffffffffffffffffffffffffffffffffff169063257b3632906084016020604051808303815f875af11580156200203f573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019062002065919062003bab565b9050805f03620020a0576040517e2f6fad00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60685463ffffffff8881166101009092041614620020ea576040517f0595ea2e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6068545f90610100900463ffffffff16620021075750896200210a565b508a5b620021336200212a848c8c8c8c8c8c8c604051620008b1929190620037b0565b8f8f8462001c30565b6200216a576040517fe0417cec00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5050505050505050505050505050565b60405173ffffffffffffffffffffffffffffffffffffffff831660248201526044810182905262001eba9084907fa9059cbb00000000000000000000000000000000000000000000000000000000906064015b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009093169290921790915262002ce8565b5f54610100900460ff16620022e8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401620012d8565b620006e062002dfa565b60026001540362002360576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c006044820152606401620012d8565b6002600155565b5f62002377600482848662003dc2565b620023829162003deb565b90507f2afa5331000000000000000000000000000000000000000000000000000000007fffffffff00000000000000000000000000000000000000000000000000000000821601620025fc575f808080808080620023e4896004818d62003dc2565b810190620023f3919062003e34565b96509650965096509650965096503373ffffffffffffffffffffffffffffffffffffffff168773ffffffffffffffffffffffffffffffffffffffff161462002467576040517f912ecce700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff86163014620024b7576040517f750643af00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8a8514620024f1576040517f03fffc4b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040805173ffffffffffffffffffffffffffffffffffffffff89811660248301528881166044830152606482018890526084820187905260ff861660a483015260c4820185905260e48083018590528351808403909101815261010490920183526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fd505accf000000000000000000000000000000000000000000000000000000001790529151918e1691620025ac919062003810565b5f604051808303815f865af19150503d805f8114620025e7576040519150601f19603f3d011682016040523d82523d5f602084013e620025ec565b606091505b50505050505050505050620009a4565b7fffffffff0000000000000000000000000000000000000000000000000000000081167f8fcbaf0c000000000000000000000000000000000000000000000000000000001462002678576040517fe282c0ba00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f808080808080806200268f8a6004818e62003dc2565b8101906200269e919062003e8a565b975097509750975097509750975097503373ffffffffffffffffffffffffffffffffffffffff168873ffffffffffffffffffffffffffffffffffffffff161462002714576040517f912ecce700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8716301462002764576040517f750643af00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040805173ffffffffffffffffffffffffffffffffffffffff8a811660248301528981166044830152606482018990526084820188905286151560a483015260ff861660c483015260e482018590526101048083018590528351808403909101815261012490920183526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f8fcbaf0c000000000000000000000000000000000000000000000000000000001790529151918f169162002828919062003810565b5f604051808303815f865af19150503d805f811462002863576040519150601f19603f3d011682016040523d82523d5f602084013e62002868565b606091505b50505050505050505050505050505050565b60405173ffffffffffffffffffffffffffffffffffffffff80851660248301528316604482015260648101829052620014589085907f23b872dd0000000000000000000000000000000000000000000000000000000090608401620021cd565b60408051600481526024810182526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f06fdde030000000000000000000000000000000000000000000000000000000017905290516060915f91829173ffffffffffffffffffffffffffffffffffffffff8616916200295d919062003810565b5f60405180830381855afa9150503d805f811462002997576040519150601f19603f3d011682016040523d82523d5f602084013e6200299c565b606091505b509150915081620029e3576040518060400160405280600781526020017f4e4f5f4e414d450000000000000000000000000000000000000000000000000081525062001d10565b62001d108162002e92565b60408051600481526024810182526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f95d89b410000000000000000000000000000000000000000000000000000000017905290516060915f91829173ffffffffffffffffffffffffffffffffffffffff86169162002a71919062003810565b5f60405180830381855afa9150503d805f811462002aab576040519150601f19603f3d011682016040523d82523d5f602084013e62002ab0565b606091505b509150915081620029e3576040518060400160405280600981526020017f4e4f5f53594d424f4c000000000000000000000000000000000000000000000081525062001d10565b60408051600481526024810182526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167f313ce5670000000000000000000000000000000000000000000000000000000017905290515f918291829173ffffffffffffffffffffffffffffffffffffffff86169162002b79919062003810565b5f60405180830381855afa9150503d805f811462002bb3576040519150601f19603f3d011682016040523d82523d5f602084013e62002bb8565b606091505b509150915081801562002bcc575080516020145b62002bd957601262001d10565b8080602001905181019062001d10919062003f11565b60018055565b60685460ff1662002c32576040517f5386698100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b606880547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690556040517f1e5e34eea33501aecf2ebec9fe0e884a40804275ea7fe10b2ba084c8374308b3905f90a1565b600881901c5f8181526069602052604081208054600160ff861690811b91821892839055929091908183169003620009a4576040517f646cf55800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f62002d4b826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65648152508573ffffffffffffffffffffffffffffffffffffffff166200307d9092919063ffffffff16565b80519091501562001eba578080602001905181019062002d6c919062003f2f565b62001eba576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f742073756363656564000000000000000000000000000000000000000000006064820152608401620012d8565b5f54610100900460ff1662002bef576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401620012d8565b6060604082511062002eb457818060200190518101906200077e919062003f4d565b81516020036200303f575f5b60208110801562002f0b575082818151811062002ee15762002ee162003aa5565b01602001517fff000000000000000000000000000000000000000000000000000000000000001615155b1562002f26578062002f1d8162003aff565b91505062002ec0565b805f0362002f6957505060408051808201909152601281527f4e4f545f56414c49445f454e434f44494e4700000000000000000000000000006020820152919050565b5f8167ffffffffffffffff81111562002f865762002f86620037bf565b6040519080825280601f01601f19166020018201604052801562002fb1576020820181803683370190505b5090505f5b82811015620030375784818151811062002fd45762002fd462003aa5565b602001015160f81c60f81b82828151811062002ff45762002ff462003aa5565b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a905350806200302e8162003aff565b91505062002fb6565b509392505050565b505060408051808201909152601281527f4e4f545f56414c49445f454e434f44494e470000000000000000000000000000602082015290565b919050565b606062001d1084845f85855f808673ffffffffffffffffffffffffffffffffffffffff168587604051620030b2919062003810565b5f6040518083038185875af1925050503d805f8114620030ee576040519150601f19603f3d011682016040523d82523d5f602084013e620030f3565b606091505b5091509150620031068783838762003111565b979650505050505050565b60608315620031ab5782515f03620031a35773ffffffffffffffffffffffffffffffffffffffff85163b620031a3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401620012d8565b508162001d10565b62001d108383815115620031c25781518083602001fd5b806040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401620012d8919062003fc8565b611b0d8062003fdd83390190565b803563ffffffff8116811462003078575f80fd5b73ffffffffffffffffffffffffffffffffffffffff811681146200323c575f80fd5b50565b5f806040838503121562003251575f80fd5b6200325c8362003206565b915060208301356200326e816200321a565b809150509250929050565b80151581146200323c575f80fd5b5f8083601f84011262003298575f80fd5b50813567ffffffffffffffff811115620032b0575f80fd5b602083019150836020828501011115620032c8575f80fd5b9250929050565b5f805f805f60808688031215620032e4575f80fd5b620032ef8662003206565b9450602086013562003301816200321a565b93506040860135620033138162003279565b9250606086013567ffffffffffffffff8111156200332f575f80fd5b6200333d8882890162003287565b969995985093965092949392505050565b8061040081018310156200077e575f80fd5b5f805f805f805f805f805f6105208c8e0312156200337c575f80fd5b620033888d8d6200334e565b9a50620033996104008d0162003206565b99506104208c013598506104408c01359750620033ba6104608d0162003206565b96506104808c0135620033cd816200321a565b9550620033de6104a08d0162003206565b94506104c08c0135620033f1816200321a565b93506104e08c013592506105008c013567ffffffffffffffff81111562003416575f80fd5b620034248e828f0162003287565b915080935050809150509295989b509295989b9093969950565b5f602082840312156200344f575f80fd5b81356200345c816200321a565b9392505050565b60ff811681146200323c575f80fd5b5f805f805f805f60e0888a03121562003489575f80fd5b8735620034968162003463565b9650620034a66020890162003206565b95506040880135620034b8816200321a565b9450620034c86060890162003206565b93506080880135620034da816200321a565b9699959850939692959460a0840135945060c09093013592915050565b5f805f606084860312156200350a575f80fd5b620035158462003206565b9250602084013562003527816200321a565b9150604084013562003539816200321a565b809150509250925092565b5f6020828403121562003555575f80fd5b5035919050565b5f805f805f805f60a0888a03121562003573575f80fd5b6200357e8862003206565b9650602088013562003590816200321a565b9550604088013567ffffffffffffffff80821115620035ad575f80fd5b620035bb8b838c0162003287565b909750955060608a0135915080821115620035d4575f80fd5b50620035e38a828b0162003287565b9094509250506080880135620035f98162003463565b8091505092959891949750929550565b5f805f805f805f60c0888a03121562003620575f80fd5b6200362b8862003206565b965060208801356200363d816200321a565b955060408801359450606088013562003656816200321a565b93506080880135620036688162003279565b925060a088013567ffffffffffffffff81111562003684575f80fd5b620036928a828b0162003287565b989b979a50959850939692959293505050565b5f805f806104608587031215620036ba575f80fd5b84359350620036cd86602087016200334e565b9250620036de610420860162003206565b939692955092936104400135925050565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b5f61010060ff8c16835263ffffffff808c16602085015273ffffffffffffffffffffffffffffffffffffffff808c166040860152818b166060860152808a166080860152508760a08501528160c0850152620037968285018789620036ef565b925080851660e085015250509a9950505050505050505050565b818382375f9101908152919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b5f5b8381101562003808578181015183820152602001620037ee565b50505f910152565b5f825162003823818460208701620037ec565b9190910192915050565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715620038775762003877620037bf565b604052919050565b5f67ffffffffffffffff8211156200389b576200389b620037bf565b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b5f82601f830112620038d7575f80fd5b8135620038ee620038e8826200387f565b6200382d565b81815284602083860101111562003903575f80fd5b816020850160208301375f918101602001919091529392505050565b5f805f6060848603121562003932575f80fd5b833567ffffffffffffffff808211156200394a575f80fd5b6200395887838801620038c7565b945060208601359150808211156200396e575f80fd5b506200397d86828701620038c7565b9250506040840135620035398162003463565b5f8151808452620039a9816020860160208601620037ec565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b606081525f620039ef606083018662003990565b828103602084015262003a03818662003990565b91505060ff83166040830152949350505050565b63ffffffff861681525f73ffffffffffffffffffffffffffffffffffffffff80871660208401528086166040840152506080606083015262003106608083018486620036ef565b73ffffffffffffffffffffffffffffffffffffffff8516815263ffffffff84166020820152606060408201525f62003a9b606083018486620036ef565b9695505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820362003b325762003b3262003ad2565b5060010190565b606081525f62003b4e606083018789620036ef565b828103602084015262003b63818688620036ef565b91505060ff831660408301529695505050505050565b5f835162003b8c818460208801620037ec565b83519083019062003ba2818360208801620037ec565b01949350505050565b5f6020828403121562003bbc575f80fd5b5051919050565b818103818111156200077e576200077e62003ad2565b5f61010060ff8b16835263ffffffff808b16602085015273ffffffffffffffffffffffffffffffffffffffff808b166040860152818a1660608601528089166080860152508660a08501528160c085015262003c388285018762003990565b925080851660e085015250509998505050505050505050565b600181815b8085111562003cb057817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0482111562003c945762003c9462003ad2565b8085161562003ca257918102915b93841c939080029062003c56565b509250929050565b5f8262003cc8575060016200077e565b8162003cd657505f6200077e565b816001811462003cef576002811462003cfa5762003d1a565b60019150506200077e565b60ff84111562003d0e5762003d0e62003ad2565b50506001821b6200077e565b5060208310610133831016604e8410600b841016171562003d3f575081810a6200077e565b62003d4b838362003c51565b807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0482111562003d805762003d8062003ad2565b029392505050565b5f6200345c838362003cb8565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52600160045260245ffd5b5f808585111562003dd1575f80fd5b8386111562003dde575f80fd5b5050820193919092039150565b7fffffffff00000000000000000000000000000000000000000000000000000000813581811691600485101562003e2c5780818660040360031b1b83161692505b505092915050565b5f805f805f805f60e0888a03121562003e4b575f80fd5b873562003e58816200321a565b9650602088013562003e6a816200321a565b955060408801359450606088013593506080880135620034da8162003463565b5f805f805f805f80610100898b03121562003ea3575f80fd5b883562003eb0816200321a565b9750602089013562003ec2816200321a565b96506040890135955060608901359450608089013562003ee28162003279565b935060a089013562003ef48162003463565b979a969950949793969295929450505060c08201359160e0013590565b5f6020828403121562003f22575f80fd5b81516200345c8162003463565b5f6020828403121562003f40575f80fd5b81516200345c8162003279565b5f6020828403121562003f5e575f80fd5b815167ffffffffffffffff81111562003f75575f80fd5b8201601f8101841362003f86575f80fd5b805162003f97620038e8826200387f565b81815285602083850101111562003fac575f80fd5b62003fbf826020830160208601620037ec565b95945050505050565b602081525f6200345c60208301846200399056fe61010060405234801562000011575f80fd5b5060405162001b0d38038062001b0d833981016040819052620000349162000282565b828260036200004483826200038d565b5060046200005382826200038d565b50503360c0525060ff811660e05246608081905262000072906200007f565b60a0525062000455915050565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f620000ab6200012c565b805160209182012060408051808201825260018152603160f81b90840152805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc66060820152608081018390523060a082015260c001604051602081830303815290604052805190602001209050919050565b6060600380546200013d9062000301565b80601f01602080910402602001604051908101604052809291908181526020018280546200016b9062000301565b8015620001ba5780601f106200019057610100808354040283529160200191620001ba565b820191905f5260205f20905b8154815290600101906020018083116200019c57829003601f168201915b5050505050905090565b634e487b7160e01b5f52604160045260245ffd5b5f82601f830112620001e8575f80fd5b81516001600160401b0380821115620002055762000205620001c4565b604051601f8301601f19908116603f01168101908282118183101715620002305762000230620001c4565b816040528381526020925086838588010111156200024c575f80fd5b5f91505b838210156200026f578582018301518183018401529082019062000250565b5f93810190920192909252949350505050565b5f805f6060848603121562000295575f80fd5b83516001600160401b0380821115620002ac575f80fd5b620002ba87838801620001d8565b94506020860151915080821115620002d0575f80fd5b50620002df86828701620001d8565b925050604084015160ff81168114620002f6575f80fd5b809150509250925092565b600181811c908216806200031657607f821691505b6020821081036200033557634e487b7160e01b5f52602260045260245ffd5b50919050565b601f82111562000388575f81815260208120601f850160051c81016020861015620003635750805b601f850160051c820191505b8181101562000384578281556001016200036f565b5050505b505050565b81516001600160401b03811115620003a957620003a9620001c4565b620003c181620003ba845462000301565b846200033b565b602080601f831160018114620003f7575f8415620003df5750858301515b5f19600386901b1c1916600185901b17855562000384565b5f85815260208120601f198616915b82811015620004275788860151825594840194600190910190840162000406565b50858210156200044557878501515f19600388901b60f8161c191681555b5050505050600190811b01905550565b60805160a05160c05160e05161166f6200049e5f395f61022d01525f81816102fb015281816105ad015261069401525f61052801525f818161036d01526104f2015261166f5ff3fe608060405234801561000f575f80fd5b506004361061016e575f3560e01c806370a08231116100d2578063a457c2d711610088578063d505accf11610063578063d505accf1461038f578063dd62ed3e146103a2578063ffa1ad74146103e7575f80fd5b8063a457c2d714610342578063a9059cbb14610355578063cd0d009614610368575f80fd5b806395d89b41116100b857806395d89b41146102db5780639dc29fac146102e3578063a3c573eb146102f6575f80fd5b806370a08231146102875780637ecebe00146102bc575f80fd5b806330adf81f116101275780633644e5151161010d5780633644e51514610257578063395093511461025f57806340c10f1914610272575f80fd5b806330adf81f146101ff578063313ce56714610226575f80fd5b806318160ddd1161015757806318160ddd146101b357806320606b70146101c557806323b872dd146101ec575f80fd5b806306fdde0314610172578063095ea7b314610190575b5f80fd5b61017a610423565b60405161018791906113c1565b60405180910390f35b6101a361019e366004611452565b6104b3565b6040519015158152602001610187565b6002545b604051908152602001610187565b6101b77f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f81565b6101a36101fa36600461147a565b6104cc565b6101b77f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152602001610187565b6101b76104ef565b6101a361026d366004611452565b61054a565b610285610280366004611452565b610595565b005b6101b76102953660046114b3565b73ffffffffffffffffffffffffffffffffffffffff165f9081526020819052604090205490565b6101b76102ca3660046114b3565b60056020525f908152604090205481565b61017a61066d565b6102856102f1366004611452565b61067c565b61031d7f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610187565b6101a3610350366004611452565b61074b565b6101a3610363366004611452565b61081b565b6101b77f000000000000000000000000000000000000000000000000000000000000000081565b61028561039d3660046114d3565b610828565b6101b76103b0366004611540565b73ffffffffffffffffffffffffffffffffffffffff9182165f90815260016020908152604080832093909416825291909152205490565b61017a6040518060400160405280600181526020017f310000000000000000000000000000000000000000000000000000000000000081525081565b60606003805461043290611571565b80601f016020809104026020016040519081016040528092919081815260200182805461045e90611571565b80156104a95780601f10610480576101008083540402835291602001916104a9565b820191905f5260205f20905b81548152906001019060200180831161048c57829003601f168201915b5050505050905090565b5f336104c0818585610b59565b60019150505b92915050565b5f336104d9858285610d0c565b6104e4858585610de2565b506001949350505050565b5f7f00000000000000000000000000000000000000000000000000000000000000004614610525576105204661104f565b905090565b507f000000000000000000000000000000000000000000000000000000000000000090565b335f81815260016020908152604080832073ffffffffffffffffffffffffffffffffffffffff871684529091528120549091906104c090829086906105909087906115ef565b610b59565b3373ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000161461065f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f546f6b656e577261707065643a3a6f6e6c794272696467653a204e6f7420506f60448201527f6c79676f6e5a6b45564d4272696467650000000000000000000000000000000060648201526084015b60405180910390fd5b6106698282611116565b5050565b60606004805461043290611571565b3373ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001614610741576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f546f6b656e577261707065643a3a6f6e6c794272696467653a204e6f7420506f60448201527f6c79676f6e5a6b45564d427269646765000000000000000000000000000000006064820152608401610656565b6106698282611207565b335f81815260016020908152604080832073ffffffffffffffffffffffffffffffffffffffff871684529091528120549091908381101561080e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760448201527f207a65726f0000000000000000000000000000000000000000000000000000006064820152608401610656565b6104e48286868403610b59565b5f336104c0818585610de2565b834211156108b7576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f546f6b656e577261707065643a3a7065726d69743a204578706972656420706560448201527f726d6974000000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff87165f90815260056020526040812080547f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9918a918a918a91908661091083611602565b9091555060408051602081019690965273ffffffffffffffffffffffffffffffffffffffff94851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f61097a6104ef565b6040517f19010000000000000000000000000000000000000000000000000000000000006020820152602281019190915260428101839052606201604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe081840301815282825280516020918201205f80855291840180845281905260ff89169284019290925260608301879052608083018690529092509060019060a0016020604051602081039080840390855afa158015610a3b573d5f803e3d5ffd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811615801590610ab657508973ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16145b610b42576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f546f6b656e577261707065643a3a7065726d69743a20496e76616c696420736960448201527f676e6174757265000000000000000000000000000000000000000000000000006064820152608401610656565b610b4d8a8a8a610b59565b50505050505050505050565b73ffffffffffffffffffffffffffffffffffffffff8316610bfb576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460448201527f72657373000000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff8216610c9e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f20616464726560448201527f73730000000000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff8381165f8181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a3505050565b73ffffffffffffffffffffffffffffffffffffffff8381165f908152600160209081526040808320938616835292905220547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610ddc5781811015610dcf576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610656565b610ddc8484848403610b59565b50505050565b73ffffffffffffffffffffffffffffffffffffffff8316610e85576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f20616460448201527f64726573730000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff8216610f28576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201527f65737300000000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff83165f9081526020819052604090205481811015610fdd576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e742065786365656473206260448201527f616c616e636500000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff8481165f81815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610ddc565b5f7f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f611079610423565b8051602091820120604080518082018252600181527f310000000000000000000000000000000000000000000000000000000000000090840152805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc66060820152608081018390523060a082015260c001604051602081830303815290604052805190602001209050919050565b73ffffffffffffffffffffffffffffffffffffffff8216611193576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610656565b8060025f8282546111a491906115ef565b909155505073ffffffffffffffffffffffffffffffffffffffff82165f81815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b73ffffffffffffffffffffffffffffffffffffffff82166112aa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360448201527f73000000000000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff82165f908152602081905260409020548181101561135f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60448201527f63650000000000000000000000000000000000000000000000000000000000006064820152608401610656565b73ffffffffffffffffffffffffffffffffffffffff83165f818152602081815260408083208686039055600280548790039055518581529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9101610cff565b5f6020808352835180828501525f5b818110156113ec578581018301518582016040015282016113d0565b505f6040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461144d575f80fd5b919050565b5f8060408385031215611463575f80fd5b61146c8361142a565b946020939093013593505050565b5f805f6060848603121561148c575f80fd5b6114958461142a565b92506114a36020850161142a565b9150604084013590509250925092565b5f602082840312156114c3575f80fd5b6114cc8261142a565b9392505050565b5f805f805f805f60e0888a0312156114e9575f80fd5b6114f28861142a565b96506115006020890161142a565b95506040880135945060608801359350608088013560ff81168114611523575f80fd5b9699959850939692959460a0840135945060c09093013592915050565b5f8060408385031215611551575f80fd5b61155a8361142a565b91506115686020840161142a565b90509250929050565b600181811c9082168061158557607f821691505b6020821081036115bc577f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b50919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b808201808211156104c6576104c66115c2565b5f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203611632576116326115c2565b506001019056fea2646970667358221220a04a4613834006222ac539b942dfe3284c1163f5082f3bafb302007d825cd7ff64736f6c63430008140033a2646970667358221220efc010afd4f7ef7135c2d52e7832f4d0832c536dbd0768cad0ec9406e70e02b864736f6c63430008140033" + }, + { + "contractName": "PolygonZkEVMBridge proxy", + "balance": "200000000000000000000000000", + "nonce": "1", + "address": "0xA34BBAf52eE84Cd95a6d5Ac2Eab9de142D4cdB53", + "bytecode": "0x60806040526004361061005d575f3560e01c80635c60da1b116100425780635c60da1b146100a65780638f283970146100e3578063f851a440146101025761006c565b80633659cfe6146100745780634f1ef286146100935761006c565b3661006c5761006a610116565b005b61006a610116565b34801561007f575f80fd5b5061006a61008e366004610854565b610130565b61006a6100a136600461086d565b610178565b3480156100b1575f80fd5b506100ba6101eb565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b3480156100ee575f80fd5b5061006a6100fd366004610854565b610228565b34801561010d575f80fd5b506100ba610255565b61011e610282565b61012e610129610359565b610362565b565b610138610380565b73ffffffffffffffffffffffffffffffffffffffff1633036101705761016d8160405180602001604052805f8152505f6103bf565b50565b61016d610116565b610180610380565b73ffffffffffffffffffffffffffffffffffffffff1633036101e3576101de8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f92019190915250600192506103bf915050565b505050565b6101de610116565b5f6101f4610380565b73ffffffffffffffffffffffffffffffffffffffff16330361021d57610218610359565b905090565b610225610116565b90565b610230610380565b73ffffffffffffffffffffffffffffffffffffffff1633036101705761016d816103e9565b5f61025e610380565b73ffffffffffffffffffffffffffffffffffffffff16330361021d57610218610380565b61028a610380565b73ffffffffffffffffffffffffffffffffffffffff16330361012e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f7879207461726760648201527f6574000000000000000000000000000000000000000000000000000000000000608482015260a4015b60405180910390fd5b5f61021861044a565b365f80375f80365f845af43d5f803e80801561037c573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b5473ffffffffffffffffffffffffffffffffffffffff16919050565b6103c883610471565b5f825111806103d45750805b156101de576103e383836104bd565b50505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f610412610380565b6040805173ffffffffffffffffffffffffffffffffffffffff928316815291841660208301520160405180910390a161016d816104e9565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6103a3565b61047a816105f5565b60405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a250565b60606104e28383604051806060016040528060278152602001610977602791396106c0565b9392505050565b73ffffffffffffffffffffffffffffffffffffffff811661058c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201527f64647265737300000000000000000000000000000000000000000000000000006064820152608401610350565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff9290921691909117905550565b73ffffffffffffffffffffffffffffffffffffffff81163b610699576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e7472616374000000000000000000000000000000000000006064820152608401610350565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6105af565b60605f808573ffffffffffffffffffffffffffffffffffffffff16856040516106e9919061090b565b5f60405180830381855af49150503d805f8114610721576040519150601f19603f3d011682016040523d82523d5f602084013e610726565b606091505b509150915061073786838387610741565b9695505050505050565b606083156107d65782515f036107cf5773ffffffffffffffffffffffffffffffffffffffff85163b6107cf576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610350565b50816107e0565b6107e083836107e8565b949350505050565b8151156107f85781518083602001fd5b806040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103509190610926565b803573ffffffffffffffffffffffffffffffffffffffff8116811461084f575f80fd5b919050565b5f60208284031215610864575f80fd5b6104e28261082c565b5f805f6040848603121561087f575f80fd5b6108888461082c565b9250602084013567ffffffffffffffff808211156108a4575f80fd5b818601915086601f8301126108b7575f80fd5b8135818111156108c5575f80fd5b8760208285010111156108d6575f80fd5b6020830194508093505050509250925092565b5f5b838110156109035781810151838201526020016108eb565b50505f910152565b5f825161091c8184602087016108e9565b9190910192915050565b602081525f82518060208401526109448160408501602087016108e9565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a26469706673582212202ac98acbfbb3d3ac1b74050e18c4e76db25a3ff2801ec69bf85d0c61414d502b64736f6c63430008140033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000068": "0x00000000000000a40d5f56745a118d0906a34e69aec8c0db1cb8fa0000000100", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000fe3306bb4124e90ea08af2776008592052ecb9e0", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000eae46b49f2fc2d5adfc457a19d6e34aec6eb6c3a" + } + }, + { + "contractName": "PolygonZkEVMGlobalExitRootL2 implementation", + "balance": "0", + "nonce": "1", + "address": "0x3c26Af9e85715f5767Ba566AAa4dfcD9c4a2fc1a", + "bytecode": "0x608060405234801561000f575f80fd5b506004361061004a575f3560e01c806301fd90441461004e578063257b36321461006a57806333d6247d14610089578063a3c573eb1461009e575b5f80fd5b61005760015481565b6040519081526020015b60405180910390f35b61005761007836600461015e565b5f6020819052908152604090205481565b61009c61009736600461015e565b6100ea565b005b6100c57f000000000000000000000000a34bbaf52ee84cd95a6d5ac2eab9de142d4cdb5381565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610061565b3373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000a34bbaf52ee84cd95a6d5ac2eab9de142d4cdb531614610159576040517fb49365dd00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600155565b5f6020828403121561016e575f80fd5b503591905056fea26469706673582212205108c6c4f924146b736832a1bdf696e20d900450207b7452462368d150f2c71c64736f6c63430008140033" + }, + { + "contractName": "PolygonZkEVMGlobalExitRootL2 proxy", + "balance": "0", + "nonce": "1", + "address": "0xa40d5f56745a118d0906a34e69aec8c0db1cb8fa", + "bytecode": "0x60806040523661001357610011610017565b005b6100115b61001f6101b7565b6001600160a01b0316336001600160a01b0316141561016f5760606001600160e01b031960003516631b2ce7f360e11b8114156100655761005e6101ea565b9150610167565b6001600160e01b0319811663278f794360e11b14156100865761005e610241565b6001600160e01b031981166308f2839760e41b14156100a75761005e610287565b6001600160e01b031981166303e1469160e61b14156100c85761005e6102b8565b6001600160e01b03198116635c60da1b60e01b14156100e95761005e6102f8565b60405162461bcd60e51b815260206004820152604260248201527f5472616e73706172656e745570677261646561626c6550726f78793a2061646d60448201527f696e2063616e6e6f742066616c6c6261636b20746f2070726f78792074617267606482015261195d60f21b608482015260a4015b60405180910390fd5b815160208301f35b61017761030c565b565b606061019e83836040518060600160405280602781526020016108576027913961031c565b9392505050565b90565b6001600160a01b03163b151590565b60007fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b60606101f4610394565b600061020336600481846106a2565b81019061021091906106e8565b905061022d8160405180602001604052806000815250600061039f565b505060408051602081019091526000815290565b606060008061025336600481846106a2565b8101906102609190610719565b915091506102708282600161039f565b604051806020016040528060008152509250505090565b6060610291610394565b60006102a036600481846106a2565b8101906102ad91906106e8565b905061022d816103cb565b60606102c2610394565b60006102cc6101b7565b604080516001600160a01b03831660208201529192500160405160208183030381529060405291505090565b6060610302610394565b60006102cc610422565b610177610317610422565b610431565b6060600080856001600160a01b0316856040516103399190610807565b600060405180830381855af49150503d8060008114610374576040519150601f19603f3d011682016040523d82523d6000602084013e610379565b606091505b509150915061038a86838387610455565b9695505050505050565b341561017757600080fd5b6103a8836104d3565b6000825111806103b55750805b156103c6576103c48383610179565b505b505050565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6103f46101b7565b604080516001600160a01b03928316815291841660208301520160405180910390a161041f81610513565b50565b600061042c6105bc565b905090565b3660008037600080366000845af43d6000803e808015610450573d6000f35b3d6000fd5b606083156104c15782516104ba576001600160a01b0385163b6104ba5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161015e565b50816104cb565b6104cb83836105e4565b949350505050565b6104dc8161060e565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b6001600160a01b0381166105785760405162461bcd60e51b815260206004820152602660248201527f455243313936373a206e65772061646d696e20697320746865207a65726f206160448201526564647265737360d01b606482015260840161015e565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b80546001600160a01b0319166001600160a01b039290921691909117905550565b60007f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6101db565b8151156105f45781518083602001fd5b8060405162461bcd60e51b815260040161015e9190610823565b6001600160a01b0381163b61067b5760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b606482015260840161015e565b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61059b565b600080858511156106b257600080fd5b838611156106bf57600080fd5b5050820193919092039150565b80356001600160a01b03811681146106e357600080fd5b919050565b6000602082840312156106fa57600080fd5b61019e826106cc565b634e487b7160e01b600052604160045260246000fd5b6000806040838503121561072c57600080fd5b610735836106cc565b9150602083013567ffffffffffffffff8082111561075257600080fd5b818501915085601f83011261076657600080fd5b81358181111561077857610778610703565b604051601f8201601f19908116603f011681019083821181831017156107a0576107a0610703565b816040528281528860208487010111156107b957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b60005b838110156107f65781810151838201526020016107de565b838111156103c45750506000910152565b600082516108198184602087016107db565b9190910192915050565b60208152600082518060208401526108428160408501602087016107db565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a264697066735822122012bb4f564f73959a03513dc74fc3c6e40e8386e6f02c16b78d6db00ce0aa16af64736f6c63430008090033", + "storage": { + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000fe3306bb4124e90ea08af2776008592052ecb9e0", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000003c26af9e85715f5767ba566aaa4dfcd9c4a2fc1a" + } + }, + { + "contractName": "PolygonZkEVMTimelock", + "balance": "0", + "nonce": "1", + "address": "0x7D38458ED6b9B04A999E86057143C5fAa0D259E9", + "bytecode": "0x6080604052600436106101bd575f3560e01c806364d62353116100f2578063b1c5f42711610092578063d547741f11610062578063d547741f1461063a578063e38335e514610659578063f23a6e611461066c578063f27a0c92146106b0575f80fd5b8063b1c5f4271461058d578063bc197c81146105ac578063c4d252f5146105f0578063d45c44351461060f575f80fd5b80638f61f4f5116100cd5780638f61f4f5146104c557806391d14854146104f8578063a217fddf14610547578063b08e51c01461055a575f80fd5b806364d62353146104685780638065657f146104875780638f2a0bb0146104a6575f80fd5b8063248a9ca31161015d57806331d507501161013857806331d50750146103b357806336568abe146103d25780633a6aae72146103f1578063584b153e14610449575f80fd5b8063248a9ca3146103375780632ab0f529146103655780632f2ff15d14610394575f80fd5b80630d3cf6fc116101985780630d3cf6fc1461025e578063134008d31461029157806313bc9f20146102a4578063150b7a02146102c3575f80fd5b806301d5062a146101c857806301ffc9a7146101e957806307bd02651461021d575f80fd5b366101c457005b5f80fd5b3480156101d3575f80fd5b506101e76101e2366004611bf6565b6106c4565b005b3480156101f4575f80fd5b50610208610203366004611c65565b610757565b60405190151581526020015b60405180910390f35b348015610228575f80fd5b506102507fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e6381565b604051908152602001610214565b348015610269575f80fd5b506102507f5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca581565b6101e761029f366004611ca4565b6107b2565b3480156102af575f80fd5b506102086102be366004611d0b565b6108a7565b3480156102ce575f80fd5b506103066102dd366004611e28565b7f150b7a0200000000000000000000000000000000000000000000000000000000949350505050565b6040517fffffffff000000000000000000000000000000000000000000000000000000009091168152602001610214565b348015610342575f80fd5b50610250610351366004611d0b565b5f9081526020819052604090206001015490565b348015610370575f80fd5b5061020861037f366004611d0b565b5f908152600160208190526040909120541490565b34801561039f575f80fd5b506101e76103ae366004611e8c565b6108cc565b3480156103be575f80fd5b506102086103cd366004611d0b565b6108f5565b3480156103dd575f80fd5b506101e76103ec366004611e8c565b61090d565b3480156103fc575f80fd5b506104247f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610214565b348015610454575f80fd5b50610208610463366004611d0b565b6109c5565b348015610473575f80fd5b506101e7610482366004611d0b565b6109da565b348015610492575f80fd5b506102506104a1366004611ca4565b610aaa565b3480156104b1575f80fd5b506101e76104c0366004611ef7565b610ae8565b3480156104d0575f80fd5b506102507fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc181565b348015610503575f80fd5b50610208610512366004611e8c565b5f9182526020828152604080842073ffffffffffffffffffffffffffffffffffffffff93909316845291905290205460ff1690565b348015610552575f80fd5b506102505f81565b348015610565575f80fd5b506102507ffd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f78381565b348015610598575f80fd5b506102506105a7366004611fa0565b610d18565b3480156105b7575f80fd5b506103066105c63660046120be565b7fbc197c810000000000000000000000000000000000000000000000000000000095945050505050565b3480156105fb575f80fd5b506101e761060a366004611d0b565b610d5c565b34801561061a575f80fd5b50610250610629366004611d0b565b5f9081526001602052604090205490565b348015610645575f80fd5b506101e7610654366004611e8c565b610e56565b6101e7610667366004611fa0565b610e7a565b348015610677575f80fd5b50610306610686366004612161565b7ff23a6e610000000000000000000000000000000000000000000000000000000095945050505050565b3480156106bb575f80fd5b50610250611121565b7fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc16106ee81611200565b5f6106fd898989898989610aaa565b9050610709818461120d565b5f817f4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca8b8b8b8b8b8a60405161074496959493929190612208565b60405180910390a3505050505050505050565b5f7fffffffff0000000000000000000000000000000000000000000000000000000082167f4e2312e00000000000000000000000000000000000000000000000000000000014806107ac57506107ac82611359565b92915050565b5f80527fdae2aa361dfd1ca020a396615627d436107c35eff9fe7738a3512819782d70696020527f5ba6852781629bcdcd4bdaa6de76d786f1c64b16acdac474e55bebc0ea157951547fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e639060ff1661082e5761082e81336113ef565b5f61083d888888888888610aaa565b905061084981856114a6565b610855888888886115e2565b5f817fc2617efa69bab66782fa219543714338489c4e9e178271560a91b82c3f612b588a8a8a8a60405161088c9493929190612252565b60405180910390a361089d816116e2565b5050505050505050565b5f818152600160205260408120546001811180156108c55750428111155b9392505050565b5f828152602081905260409020600101546108e681611200565b6108f0838361178a565b505050565b5f8181526001602052604081205481905b1192915050565b73ffffffffffffffffffffffffffffffffffffffff811633146109b7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201527f20726f6c657320666f722073656c66000000000000000000000000000000000060648201526084015b60405180910390fd5b6109c18282611878565b5050565b5f818152600160208190526040822054610906565b333014610a69576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f54696d656c6f636b436f6e74726f6c6c65723a2063616c6c6572206d7573742060448201527f62652074696d656c6f636b00000000000000000000000000000000000000000060648201526084016109ae565b60025460408051918252602082018390527f11c24f4ead16507c69ac467fbd5e4eed5fb5c699626d2cc6d66421df253886d5910160405180910390a1600255565b5f868686868686604051602001610ac696959493929190612208565b6040516020818303038152906040528051906020012090509695505050505050565b7fb09aa5aeb3702cfd50b6b62bc4532604938f21248a27a1d5ca736082b6819cc1610b1281611200565b888714610ba1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f54696d656c6f636b436f6e74726f6c6c65723a206c656e677468206d69736d6160448201527f746368000000000000000000000000000000000000000000000000000000000060648201526084016109ae565b888514610c30576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f54696d656c6f636b436f6e74726f6c6c65723a206c656e677468206d69736d6160448201527f746368000000000000000000000000000000000000000000000000000000000060648201526084016109ae565b5f610c418b8b8b8b8b8b8b8b610d18565b9050610c4d818461120d565b5f5b8a811015610d0a5780827f4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca8e8e85818110610c8c57610c8c612291565b9050602002016020810190610ca191906122be565b8d8d86818110610cb357610cb3612291565b905060200201358c8c87818110610ccc57610ccc612291565b9050602002810190610cde91906122d7565b8c8b604051610cf296959493929190612208565b60405180910390a3610d0381612365565b9050610c4f565b505050505050505050505050565b5f8888888888888888604051602001610d38989796959493929190612447565b60405160208183030381529060405280519060200120905098975050505050505050565b7ffd643c72710c63c0180259aba6b2d05451e3591a24e58b62239378085726f783610d8681611200565b610d8f826109c5565b610e1b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603160248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e20636160448201527f6e6e6f742062652063616e63656c6c656400000000000000000000000000000060648201526084016109ae565b5f828152600160205260408082208290555183917fbaa1eb22f2a492ba1a5fea61b8df4d27c6c8b5f3971e63bb58fa14ff72eedb7091a25050565b5f82815260208190526040902060010154610e7081611200565b6108f08383611878565b5f80527fdae2aa361dfd1ca020a396615627d436107c35eff9fe7738a3512819782d70696020527f5ba6852781629bcdcd4bdaa6de76d786f1c64b16acdac474e55bebc0ea157951547fd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e639060ff16610ef657610ef681336113ef565b878614610f85576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f54696d656c6f636b436f6e74726f6c6c65723a206c656e677468206d69736d6160448201527f746368000000000000000000000000000000000000000000000000000000000060648201526084016109ae565b878414611014576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f54696d656c6f636b436f6e74726f6c6c65723a206c656e677468206d69736d6160448201527f746368000000000000000000000000000000000000000000000000000000000060648201526084016109ae565b5f6110258a8a8a8a8a8a8a8a610d18565b905061103181856114a6565b5f5b8981101561110b575f8b8b8381811061104e5761104e612291565b905060200201602081019061106391906122be565b90505f8a8a8481811061107857611078612291565b905060200201359050365f8a8a8681811061109557611095612291565b90506020028101906110a791906122d7565b915091506110b7848484846115e2565b84867fc2617efa69bab66782fa219543714338489c4e9e178271560a91b82c3f612b58868686866040516110ee9493929190612252565b60405180910390a3505050508061110490612365565b9050611033565b50611115816116e2565b50505050505050505050565b5f7f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff16158015906111ef57507f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff166315064c966040518163ffffffff1660e01b8152600401602060405180830381865afa1580156111cb573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906111ef919061250c565b156111f957505f90565b5060025490565b61120a81336113ef565b50565b611216826108f5565b156112a3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602f60248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e20616c60448201527f7265616479207363686564756c6564000000000000000000000000000000000060648201526084016109ae565b6112ab611121565b81101561133a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f54696d656c6f636b436f6e74726f6c6c65723a20696e73756666696369656e7460448201527f2064656c6179000000000000000000000000000000000000000000000000000060648201526084016109ae565b611344814261252b565b5f928352600160205260409092209190915550565b5f7fffffffff0000000000000000000000000000000000000000000000000000000082167f7965db0b0000000000000000000000000000000000000000000000000000000014806107ac57507f01ffc9a7000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000008316146107ac565b5f8281526020818152604080832073ffffffffffffffffffffffffffffffffffffffff8516845290915290205460ff166109c15761142c8161192d565b61143783602061194c565b604051602001611448929190612560565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0818403018152908290527f08c379a00000000000000000000000000000000000000000000000000000000082526109ae916004016125e0565b6114af826108a7565b61153b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e20697360448201527f206e6f742072656164790000000000000000000000000000000000000000000060648201526084016109ae565b80158061155657505f81815260016020819052604090912054145b6109c1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f54696d656c6f636b436f6e74726f6c6c65723a206d697373696e67206465706560448201527f6e64656e6379000000000000000000000000000000000000000000000000000060648201526084016109ae565b5f8473ffffffffffffffffffffffffffffffffffffffff1684848460405161160b929190612630565b5f6040518083038185875af1925050503d805f8114611645576040519150601f19603f3d011682016040523d82523d5f602084013e61164a565b606091505b50509050806116db576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603360248201527f54696d656c6f636b436f6e74726f6c6c65723a20756e6465726c79696e67207460448201527f72616e73616374696f6e2072657665727465640000000000000000000000000060648201526084016109ae565b5050505050565b6116eb816108a7565b611777576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602a60248201527f54696d656c6f636b436f6e74726f6c6c65723a206f7065726174696f6e20697360448201527f206e6f742072656164790000000000000000000000000000000000000000000060648201526084016109ae565b5f90815260016020819052604090912055565b5f8281526020818152604080832073ffffffffffffffffffffffffffffffffffffffff8516845290915290205460ff166109c1575f8281526020818152604080832073ffffffffffffffffffffffffffffffffffffffff85168452909152902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905561181a3390565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b5f8281526020818152604080832073ffffffffffffffffffffffffffffffffffffffff8516845290915290205460ff16156109c1575f8281526020818152604080832073ffffffffffffffffffffffffffffffffffffffff8516808552925280832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905551339285917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b60606107ac73ffffffffffffffffffffffffffffffffffffffff831660145b60605f61195a83600261263f565b61196590600261252b565b67ffffffffffffffff81111561197d5761197d611d22565b6040519080825280601f01601f1916602001820160405280156119a7576020820181803683370190505b5090507f3000000000000000000000000000000000000000000000000000000000000000815f815181106119dd576119dd612291565b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053507f780000000000000000000000000000000000000000000000000000000000000081600181518110611a3f57611a3f612291565b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f611a7984600261263f565b611a8490600161252b565b90505b6001811115611b20577f303132333435363738396162636465660000000000000000000000000000000085600f1660108110611ac557611ac5612291565b1a60f81b828281518110611adb57611adb612291565b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a90535060049490941c93611b1981612656565b9050611a87565b5083156108c5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e7460448201526064016109ae565b803573ffffffffffffffffffffffffffffffffffffffff81168114611bac575f80fd5b919050565b5f8083601f840112611bc1575f80fd5b50813567ffffffffffffffff811115611bd8575f80fd5b602083019150836020828501011115611bef575f80fd5b9250929050565b5f805f805f805f60c0888a031215611c0c575f80fd5b611c1588611b89565b965060208801359550604088013567ffffffffffffffff811115611c37575f80fd5b611c438a828b01611bb1565b989b979a50986060810135976080820135975060a09091013595509350505050565b5f60208284031215611c75575f80fd5b81357fffffffff00000000000000000000000000000000000000000000000000000000811681146108c5575f80fd5b5f805f805f8060a08789031215611cb9575f80fd5b611cc287611b89565b955060208701359450604087013567ffffffffffffffff811115611ce4575f80fd5b611cf089828a01611bb1565b979a9699509760608101359660809091013595509350505050565b5f60208284031215611d1b575f80fd5b5035919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016810167ffffffffffffffff81118282101715611d9657611d96611d22565b604052919050565b5f82601f830112611dad575f80fd5b813567ffffffffffffffff811115611dc757611dc7611d22565b611df860207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f84011601611d4f565b818152846020838601011115611e0c575f80fd5b816020850160208301375f918101602001919091529392505050565b5f805f8060808587031215611e3b575f80fd5b611e4485611b89565b9350611e5260208601611b89565b925060408501359150606085013567ffffffffffffffff811115611e74575f80fd5b611e8087828801611d9e565b91505092959194509250565b5f8060408385031215611e9d575f80fd5b82359150611ead60208401611b89565b90509250929050565b5f8083601f840112611ec6575f80fd5b50813567ffffffffffffffff811115611edd575f80fd5b6020830191508360208260051b8501011115611bef575f80fd5b5f805f805f805f805f60c08a8c031215611f0f575f80fd5b893567ffffffffffffffff80821115611f26575f80fd5b611f328d838e01611eb6565b909b50995060208c0135915080821115611f4a575f80fd5b611f568d838e01611eb6565b909950975060408c0135915080821115611f6e575f80fd5b50611f7b8c828d01611eb6565b9a9d999c50979a969997986060880135976080810135975060a0013595509350505050565b5f805f805f805f8060a0898b031215611fb7575f80fd5b883567ffffffffffffffff80821115611fce575f80fd5b611fda8c838d01611eb6565b909a50985060208b0135915080821115611ff2575f80fd5b611ffe8c838d01611eb6565b909850965060408b0135915080821115612016575f80fd5b506120238b828c01611eb6565b999c989b509699959896976060870135966080013595509350505050565b5f82601f830112612050575f80fd5b8135602067ffffffffffffffff82111561206c5761206c611d22565b8160051b61207b828201611d4f565b9283528481018201928281019087851115612094575f80fd5b83870192505b848310156120b35782358252918301919083019061209a565b979650505050505050565b5f805f805f60a086880312156120d2575f80fd5b6120db86611b89565b94506120e960208701611b89565b9350604086013567ffffffffffffffff80821115612105575f80fd5b61211189838a01612041565b94506060880135915080821115612126575f80fd5b61213289838a01612041565b93506080880135915080821115612147575f80fd5b5061215488828901611d9e565b9150509295509295909350565b5f805f805f60a08688031215612175575f80fd5b61217e86611b89565b945061218c60208701611b89565b93506040860135925060608601359150608086013567ffffffffffffffff8111156121b5575f80fd5b61215488828901611d9e565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b73ffffffffffffffffffffffffffffffffffffffff8716815285602082015260a060408201525f61223d60a0830186886121c1565b60608301949094525060800152949350505050565b73ffffffffffffffffffffffffffffffffffffffff85168152836020820152606060408201525f6122876060830184866121c1565b9695505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f602082840312156122ce575f80fd5b6108c582611b89565b5f8083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe184360301811261230a575f80fd5b83018035915067ffffffffffffffff821115612324575f80fd5b602001915036819003821315611bef575f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361239557612395612338565b5060010190565b8183525f6020808501808196508560051b81019150845f5b8781101561243a57828403895281357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18836030181126123f2575f80fd5b8701858101903567ffffffffffffffff81111561240d575f80fd5b80360382131561241b575f80fd5b6124268682846121c1565b9a87019a95505050908401906001016123b4565b5091979650505050505050565b60a080825281018890525f8960c08301825b8b8110156124945773ffffffffffffffffffffffffffffffffffffffff61247f84611b89565b16825260209283019290910190600101612459565b5083810360208501528881527f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8911156124cc575f80fd5b8860051b9150818a602083013701828103602090810160408501526124f4908201878961239c565b60608401959095525050608001529695505050505050565b5f6020828403121561251c575f80fd5b815180151581146108c5575f80fd5b808201808211156107ac576107ac612338565b5f5b83811015612558578181015183820152602001612540565b50505f910152565b7f416363657373436f6e74726f6c3a206163636f756e742000000000000000000081525f835161259781601785016020880161253e565b7f206973206d697373696e6720726f6c652000000000000000000000000000000060179184019182015283516125d481602884016020880161253e565b01602801949350505050565b602081525f82518060208401526125fe81604085016020870161253e565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169190910160400192915050565b818382375f9101908152919050565b80820281158282048414176107ac576107ac612338565b5f8161266457612664612338565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019056fea2646970667358221220d904e0b10230579952f8427a107aa4349f9a1f5799108d399b11e28b578463e464736f6c63430008140033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000000000000000000000e10", + "0x6004ee6a16227fd0e871b5f914d8433417e16ad83654d913c1ba69cc35c2132e": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xf5f0a218cd667c05e379a91ca023db2743ccc055fbfe1d11963ca6b052736ab1": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x64494413541ff93b31aa309254e3fed72a7456e9845988b915b4c7a7ceba8814": "0x5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5", + "0x9db27d76cb60f110b9a2cbcd55f6b5d54993c33cefeaa1979d0bdfd0218c8e9e": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x3412d5605ac6cd444957cedb533e5dacad6378b4bc819ebe3652188a665066d6": "0x5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5", + "0x3b674ad523dadae6f60882426711ee3b02fd243e4a8e114dfdd20da2bcc3899d": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xdae2aa361dfd1ca020a396615627d436107c35eff9fe7738a3512819782d706a": "0x5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5", + "0xf825292247308a48f73de280a1dc14cbea6657baa530dcfbfa562b981b9cda2f": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0xc3ad33e20b0c56a223ad5104fff154aa010f8715b9c981fd38fdc60a4d1a52fc": "0x5f58e3a2316349923ce3780f8d587db2d72378aed66a8261c916544fa6846ca5" + } + }, + { + "accountName": "keyless Deployer", + "balance": "0", + "nonce": "1", + "address": "0x6a8F29BB59c1cf00b263691500AC93c867DAD915" + }, + { + "accountName": "deployer", + "balance": "0", + "nonce": "8", + "address": "0x56b9Eaf5D19639ACc16C6373C66e5a1F61CF29b6" + } + ], + "l1Config": { + "chainId": 11155111, + "polygonZkEVMAddress": "0xCE5622f775cF645C179B7Fe189D6a87307A11e05", + "maticTokenAddress": "0x7fee073b978A6AAe2deF14c9A2BB73BD1E39e29c", + "polygonZkEVMGlobalExitRootAddress": "0xD1709c00280E57Cb88CcA035f5f3a5f7EFdC2dfC" + }, + "genesisBlockNumber": 4482328 +} \ No newline at end of file diff --git a/apps/explorer/test/support/fixture/smart_contract/ERC677.sol b/apps/explorer/test/support/fixture/smart_contract/ERC677.sol index e999f2b306c5..f1b4cf495f76 100644 --- a/apps/explorer/test/support/fixture/smart_contract/ERC677.sol +++ b/apps/explorer/test/support/fixture/smart_contract/ERC677.sol @@ -64,7 +64,7 @@ interface IERC677MultiBridgeToken { * specific functions. * * This module is used through inheritance. It will make available the modifier -* `onlyOwner`, which can be aplied to your functions to restrict their use to +* `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ contract Ownable { @@ -1160,4 +1160,4 @@ contract ERC677MultiBridgeToken is IERC677MultiBridgeToken, ERC677BridgeToken { function isBridge(address _address) public view returns (bool) { return _address != F_ADDR && bridgePointers[_address] != address(0); } -} \ No newline at end of file +} diff --git a/apps/explorer/test/support/fixture/smart_contract/issue_3082.sol b/apps/explorer/test/support/fixture/smart_contract/issue_3082.sol index 7a06a2bfb48b..b0040d68a1a8 100644 --- a/apps/explorer/test/support/fixture/smart_contract/issue_3082.sol +++ b/apps/explorer/test/support/fixture/smart_contract/issue_3082.sol @@ -28,7 +28,7 @@ interface IERC677MultiBridgeToken { * specific functions. * * This module is used through inheritance. It will make available the modifier - * `onlyOwner`, which can be aplied to your functions to restrict their use to + * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ contract Ownable { @@ -591,4 +591,4 @@ contract Distribution is Ownable, IDistribution { revert("invalid address"); } } -} \ No newline at end of file +} diff --git a/apps/explorer/test/support/fixture/smart_contract/issue_5114.sol b/apps/explorer/test/support/fixture/smart_contract/issue_5114.sol index 0c5458210089..5a997e254e0f 100644 --- a/apps/explorer/test/support/fixture/smart_contract/issue_5114.sol +++ b/apps/explorer/test/support/fixture/smart_contract/issue_5114.sol @@ -16,7 +16,7 @@ abstract contract Proxy { /** * @dev Delegates the current call to `implementation`. * - * This function does not return to its internall call site, it will return directly to the external caller. + * This function does not return to its internal call site, it will return directly to the external caller. */ function _delegate(address implementation) internal virtual { // solhint-disable-next-line no-inline-assembly @@ -49,7 +49,7 @@ abstract contract Proxy { /** * @dev Delegates the current call to the address returned by `_implementation()`. * - * This function does not return to its internall call site, it will return directly to the external caller. + * This function does not return to its internal call site, it will return directly to the external caller. */ function _fallback() internal virtual { _beforeFallback(); diff --git a/apps/explorer/test/support/fixture/smart_contract/issue_with_constructor_args.sol b/apps/explorer/test/support/fixture/smart_contract/issue_with_constructor_args.sol index d17247212c07..0b094acf52bf 100644 --- a/apps/explorer/test/support/fixture/smart_contract/issue_with_constructor_args.sol +++ b/apps/explorer/test/support/fixture/smart_contract/issue_with_constructor_args.sol @@ -292,7 +292,7 @@ abstract contract Proxy { /** * @dev Delegates the current call to `implementation`. * - * This function does not return to its internall call site, it will return directly to the external caller. + * This function does not return to its internal call site, it will return directly to the external caller. */ function _delegate(address implementation) internal virtual { // solhint-disable-next-line no-inline-assembly @@ -325,7 +325,7 @@ abstract contract Proxy { /** * @dev Delegates the current call to the address returned by `_implementation()`. * - * This function does not return to its internall call site, it will return directly to the external caller. + * This function does not return to its internal call site, it will return directly to the external caller. */ function _fallback() internal virtual { _beforeFallback(); @@ -557,7 +557,7 @@ abstract contract ERC1967Upgrade { * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. * * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded - * function call, and allows initializating the storage of the proxy like a Solidity constructor. + * function call, and allows initializing the storage of the proxy like a Solidity constructor. */ constructor(address _logic, bytes memory _data) payable { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); @@ -570,4 +570,4 @@ abstract contract ERC1967Upgrade { function _implementation() internal view virtual override returns (address impl) { return ERC1967Upgrade._getImplementation(); } -} \ No newline at end of file +} diff --git a/apps/explorer/test/support/fixture/smart_contract/large_smart_contract.sol b/apps/explorer/test/support/fixture/smart_contract/large_smart_contract.sol index 6b147f21d86e..44f5c8f60721 100644 --- a/apps/explorer/test/support/fixture/smart_contract/large_smart_contract.sol +++ b/apps/explorer/test/support/fixture/smart_contract/large_smart_contract.sol @@ -113,7 +113,7 @@ interface IHomeWork { * @param key bytes32 The unique value used to derive the home address. * @param owner address The account that will be granted ownership of the * ERC721 token. - * @dev In order to mint an ERC721 token, the assocated home address cannot be + * @dev In order to mint an ERC721 token, the associated home address cannot be * in use, or else the token will not be able to deploy to the home address. * The controller is set to this contract until the token is redeemed, at * which point the redeemer designates a new controller for the home address. @@ -239,7 +239,7 @@ interface IHomeWork { * @param owner address The account that will be granted ownership of the * ERC721 token. * @return The derived key. - * @dev In order to mint an ERC721 token, the assocated home address cannot be + * @dev In order to mint an ERC721 token, the associated home address cannot be * in use, or else the token will not be able to deploy to the home address. * The controller is set to this contract until the token is redeemed, at * which point the redeemer designates a new controller for the home address. @@ -1778,7 +1778,7 @@ contract HomeWork is IHomeWork, ERC721Enumerable, IERC721Metadata, IERC1412 { * @param key bytes32 The unique value used to derive the home address. * @param owner address The account that will be granted ownership of the * ERC721 token. - * @dev In order to mint an ERC721 token, the assocated home address cannot be + * @dev In order to mint an ERC721 token, the associated home address cannot be * in use, or else the token will not be able to deploy to the home address. * The controller is set to this contract until the token is redeemed, at * which point the redeemer designates a new controller for the home address. @@ -2011,7 +2011,7 @@ contract HomeWork is IHomeWork, ERC721Enumerable, IERC721Metadata, IERC1412 { * @param owner address The account that will be granted ownership of the * ERC721 token. * @return The derived key. - * @dev In order to mint an ERC721 token, the assocated home address cannot be + * @dev In order to mint an ERC721 token, the associated home address cannot be * in use, or else the token will not be able to deploy to the home address. * The controller is set to this contract until the token is redeemed, at * which point the redeemer designates a new controller for the home address. @@ -2886,7 +2886,7 @@ contract HomeWork is IHomeWork, ERC721Enumerable, IERC721Metadata, IERC1412 { * * data:application/json,{ * "name":"Home%20Address%20-%200x********************", - * "description":"< ... HomeWork NFT desription ... >", + * "description":"< ... HomeWork NFT description ... >", * "image":"data:image/svg+xml;charset=utf-8;base64,< ... Image ... >"} * * where ******************** represents the checksummed home address that the @@ -3063,7 +3063,7 @@ contract HomeWork is IHomeWork, ERC721Enumerable, IERC721Metadata, IERC1412 { /** * @notice Internal function for deploying arbitrary contract code to the home - * address corresponding to a suppied key via metamorphic initialization code. + * address corresponding to a supplied key via metamorphic initialization code. * @return The home address and the hash of the deployed runtime code. * @dev This deployment method uses the "metamorphic delegator" pattern, where * it will retrieve the address of the contract that contains the target @@ -3793,7 +3793,7 @@ contract HomeWorkDeployer { /** * @notice Internal function for deploying arbitrary contract code to the home - * address corresponding to a suppied key via metamorphic initialization code. + * address corresponding to a supplied key via metamorphic initialization code. * @dev This deployment method uses the "metamorphic delegator" pattern, where * it will retrieve the address of the contract that contains the target * initialization code, then delegatecall into it, which executes the @@ -3871,4 +3871,4 @@ contract HomeWorkDeployer { require(!_disabled, "Contract is disabled."); _; } -} \ No newline at end of file +} diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index 938420e729a0..90d21435e5d6 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -16,7 +16,12 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonEdge, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonZkevm, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.RSK, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Shibarium, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Suave, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Beacon, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :auto) Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source) Mox.defmock(Explorer.Market.History.Source.Price.TestSource, for: Explorer.Market.History.Source.Price) diff --git a/apps/indexer/README.md b/apps/indexer/README.md index 60d286cdf533..5a22018299a6 100644 --- a/apps/indexer/README.md +++ b/apps/indexer/README.md @@ -24,6 +24,7 @@ Some data has to be extracted from already fetched data, and there're several tr - `transaction_actions`: parses logs to extract transaction actions - `address_token_balances`: creates token balance entities for further fetching, based on detected token transfers - `blocks`: extracts block signer hash from additional data for Clique chains +- `optimism_withdrawals`: parses logs to extract L2 withdrawal messages ### Root fetchers @@ -31,6 +32,11 @@ Some data has to be extracted from already fetched data, and there're several tr - `block/realtime`: listens for new blocks from websocket and polls node for new blocks, imports new ones one by one - `block/catchup`: gets unfetched ranges of blocks, imports them in batches - `transaction_action`: optionally fetches/rewrites transaction actions for old blocks (in a given range of blocks for given protocols) +- `optimism/txn_batch`: fetches transaction batches of Optimism chain +- `optimism/output_root`: fetches output roots of Optimism chain +- `optimism/deposit`: fetches deposits to Optimism chain +- `optimism/withdrawal`: fetches withdrawals from Optimism chain +- `optimism/withdrawal_event`: fetches withdrawal events on L1 chain - `withdrawals`: optionally fetches withdrawals for old blocks (in the given from boundary of block numbers) Both block fetchers retrieve/extract the blocks themselves and the following additional data: diff --git a/apps/indexer/config/dev/arbitrum.exs b/apps/indexer/config/dev/arbitrum.exs index 18125586af44..ee13d8ff9be2 100644 --- a/apps/indexer/config/dev/arbitrum.exs +++ b/apps/indexer/config/dev/arbitrum.exs @@ -19,6 +19,10 @@ config :indexer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545") + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Arbitrum diff --git a/apps/indexer/config/dev/besu.exs b/apps/indexer/config/dev/besu.exs index c6d8d4a1aa00..e1258bff85cb 100644 --- a/apps/indexer/config/dev/besu.exs +++ b/apps/indexer/config/dev/besu.exs @@ -21,7 +21,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", diff --git a/apps/indexer/config/dev/erigon.exs b/apps/indexer/config/dev/erigon.exs index 912b64dde2ec..2a7def995444 100644 --- a/apps/indexer/config/dev/erigon.exs +++ b/apps/indexer/config/dev/erigon.exs @@ -21,7 +21,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", diff --git a/apps/indexer/config/dev/filecoin.exs b/apps/indexer/config/dev/filecoin.exs new file mode 100644 index 000000000000..65fab05db094 --- /dev/null +++ b/apps/indexer/config/dev/filecoin.exs @@ -0,0 +1,40 @@ +import Config + +~w(config config_helper.exs) +|> Path.join() +|> Code.eval_file() + +hackney_opts = ConfigHelper.hackney_options() +timeout = ConfigHelper.timeout(1) + +config :indexer, + block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "5s"), + json_rpc_named_arguments: [ + transport: + if(System.get_env("ETHEREUM_JSONRPC_TRANSPORT", "http") == "http", + do: EthereumJSONRPC.HTTP, + else: EthereumJSONRPC.IPC + ), + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:1234/rpc/v1", + fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:1234/rpc/v1"), + trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:1234/rpc/v1" + ], + http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + ], + variant: EthereumJSONRPC.Filecoin + ], + subscribe_named_arguments: [ + transport: + System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + EthereumJSONRPC.WebSocket, + transport_options: [ + web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, + url: System.get_env("ETHEREUM_JSONRPC_WS_URL") + ] + ] diff --git a/apps/indexer/config/dev/ganache.exs b/apps/indexer/config/dev/ganache.exs index be2cd745b191..fcb3e127e122 100644 --- a/apps/indexer/config/dev/ganache.exs +++ b/apps/indexer/config/dev/ganache.exs @@ -19,6 +19,10 @@ config :indexer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:7545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:7545") + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Ganache diff --git a/apps/indexer/config/dev/geth.exs b/apps/indexer/config/dev/geth.exs index 87b9dbe90613..c8182d931342 100644 --- a/apps/indexer/config/dev/geth.exs +++ b/apps/indexer/config/dev/geth.exs @@ -20,8 +20,11 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), + debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", + debug_traceBlockByNumber: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], diff --git a/apps/indexer/config/dev/nethermind.exs b/apps/indexer/config/dev/nethermind.exs index a00a7acd1781..50c7eec22d2e 100644 --- a/apps/indexer/config/dev/nethermind.exs +++ b/apps/indexer/config/dev/nethermind.exs @@ -21,7 +21,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", diff --git a/apps/indexer/config/dev/rsk.exs b/apps/indexer/config/dev/rsk.exs index 5168fdbbff10..d4e4b42630fb 100644 --- a/apps/indexer/config/dev/rsk.exs +++ b/apps/indexer/config/dev/rsk.exs @@ -22,7 +22,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545"), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", diff --git a/apps/indexer/config/prod/arbitrum.exs b/apps/indexer/config/prod/arbitrum.exs index cef49883b46e..a4d0667d6384 100644 --- a/apps/indexer/config/prod/arbitrum.exs +++ b/apps/indexer/config/prod/arbitrum.exs @@ -19,6 +19,10 @@ config :indexer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:8545") + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Arbitrum diff --git a/apps/indexer/config/prod/besu.exs b/apps/indexer/config/prod/besu.exs index 7970b1207799..0f98fc1d6775 100644 --- a/apps/indexer/config/prod/besu.exs +++ b/apps/indexer/config/prod/besu.exs @@ -20,7 +20,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), diff --git a/apps/indexer/config/prod/erigon.exs b/apps/indexer/config/prod/erigon.exs index 27b2c410a96c..f01fbc77409b 100644 --- a/apps/indexer/config/prod/erigon.exs +++ b/apps/indexer/config/prod/erigon.exs @@ -20,7 +20,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), diff --git a/apps/indexer/config/prod/filecoin.exs b/apps/indexer/config/prod/filecoin.exs new file mode 100644 index 000000000000..fc62d266cc13 --- /dev/null +++ b/apps/indexer/config/prod/filecoin.exs @@ -0,0 +1,40 @@ +import Config + +~w(config config_helper.exs) +|> Path.join() +|> Code.eval_file() + +hackney_opts = ConfigHelper.hackney_options() +timeout = ConfigHelper.timeout(10) + +config :indexer, + block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "5s"), + json_rpc_named_arguments: [ + transport: + if(System.get_env("ETHEREUM_JSONRPC_TRANSPORT", "http") == "http", + do: EthereumJSONRPC.HTTP, + else: EthereumJSONRPC.IPC + ), + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), + fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url(), + trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") + ], + http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + ], + variant: EthereumJSONRPC.Filecoin + ], + subscribe_named_arguments: [ + transport: + System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + EthereumJSONRPC.WebSocket, + transport_options: [ + web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, + url: System.get_env("ETHEREUM_JSONRPC_WS_URL") + ] + ] diff --git a/apps/indexer/config/prod/ganache.exs b/apps/indexer/config/prod/ganache.exs index be2cd745b191..fcb3e127e122 100644 --- a/apps/indexer/config/prod/ganache.exs +++ b/apps/indexer/config/prod/ganache.exs @@ -19,6 +19,10 @@ config :indexer, http: EthereumJSONRPC.HTTP.HTTPoison, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:7545", fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), + method_to_url: [ + eth_call: ConfigHelper.eth_call_url("http://localhost:7545") + ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], variant: EthereumJSONRPC.Ganache diff --git a/apps/indexer/config/prod/geth.exs b/apps/indexer/config/prod/geth.exs index f0b57f9b902f..9e3ccb4e12c3 100644 --- a/apps/indexer/config/prod/geth.exs +++ b/apps/indexer/config/prod/geth.exs @@ -20,8 +20,11 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ - debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") + eth_call: ConfigHelper.eth_call_url(), + debug_traceTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), + debug_traceBlockByNumber: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") ], http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] ], diff --git a/apps/indexer/config/prod/nethermind.exs b/apps/indexer/config/prod/nethermind.exs index baed67a6913d..4ba53bc7d143 100644 --- a/apps/indexer/config/prod/nethermind.exs +++ b/apps/indexer/config/prod/nethermind.exs @@ -20,7 +20,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), diff --git a/apps/indexer/config/prod/rsk.exs b/apps/indexer/config/prod/rsk.exs index f9090ddd3760..ad4ce94b39d6 100644 --- a/apps/indexer/config/prod/rsk.exs +++ b/apps/indexer/config/prod/rsk.exs @@ -22,7 +22,9 @@ config :indexer, url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_HTTP_URL"), fallback_trace_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_TRACE_URL"), + fallback_eth_call_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL"), method_to_url: [ + eth_call: ConfigHelper.eth_call_url(), eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_block: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), trace_replayBlockTransactions: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), diff --git a/apps/indexer/config/runtime/test.exs b/apps/indexer/config/runtime/test.exs index e2043f6c1435..7c9daee034fc 100644 --- a/apps/indexer/config/runtime/test.exs +++ b/apps/indexer/config/runtime/test.exs @@ -2,6 +2,9 @@ import Config alias EthereumJSONRPC.Variant +config :indexer, Indexer.Fetcher.Beacon.Blob.Supervisor, disabled?: true +config :indexer, Indexer.Fetcher.Beacon.Blob, start_block: 0 + variant = Variant.get() Code.require_file("#{variant}.exs", "#{__DIR__}/../../../explorer/config/test") diff --git a/apps/indexer/config/test/filecoin.exs b/apps/indexer/config/test/filecoin.exs new file mode 100644 index 000000000000..a7509d5e827e --- /dev/null +++ b/apps/indexer/config/test/filecoin.exs @@ -0,0 +1,8 @@ +import Config + +config :indexer, + json_rpc_named_arguments: [ + transport: EthereumJSONRPC.Mox, + transport_options: [], + variant: EthereumJSONRPC.Filecoin + ] diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index 3b5fcbbc31fa..de26da59c552 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -9,6 +9,7 @@ defmodule Indexer.Block.Catchup.Fetcher do import Indexer.Block.Fetcher, only: [ + async_import_blobs: 1, async_import_block_rewards: 1, async_import_coin_balances: 2, async_import_created_contract_codes: 1, @@ -23,7 +24,8 @@ defmodule Indexer.Block.Catchup.Fetcher do alias Ecto.Changeset alias Explorer.Chain - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Chain.NullRoundHeight + alias Explorer.Utility.{MassiveBlock, MissingRangesManipulator} alias Indexer.{Block, Tracer} alias Indexer.Block.Catchup.{Sequence, TaskSupervisor} alias Indexer.Memory.Shrinkable @@ -163,6 +165,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_uncles(imported) async_import_replaced_transactions(imported) async_import_token_instances(imported) + async_import_blobs(imported) end defp stream_fetch_and_import(state, sequence) @@ -198,7 +201,8 @@ defmodule Indexer.Block.Catchup.Fetcher do case result do {:ok, %{inserted: inserted, errors: errors}} -> - errors = cap_seq(sequence, errors) + valid_errors = handle_null_rounds(errors) + errors = cap_seq(sequence, valid_errors) retry(sequence, errors) clear_missing_ranges(range, errors) @@ -215,6 +219,7 @@ defmodule Indexer.Block.Catchup.Fetcher do {:error, {:import = step, reason}} = error -> Prometheus.Instrumenter.import_errors() Logger.error(fn -> [inspect(reason), ". Retrying."] end, step: step) + if reason == :timeout, do: add_range_to_massive_blocks(range) push_back(sequence, range) @@ -246,10 +251,39 @@ defmodule Indexer.Block.Catchup.Fetcher do end rescue exception -> + if timeout_exception?(exception), do: add_range_to_massive_blocks(range) Logger.error(fn -> [Exception.format(:error, exception, __STACKTRACE__), ?\n, ?\n, "Retrying."] end) {:error, exception} end + defp handle_null_rounds(errors) do + {null_rounds, other_errors} = + Enum.split_with(errors, fn + %{message: "requested epoch was a null round"} -> true + _ -> false + end) + + null_rounds + |> Enum.map(&block_error_to_number/1) + |> NullRoundHeight.insert_heights() + + other_errors + end + + defp timeout_exception?(%{message: message}) when is_binary(message) do + String.match?(message, ~r/due to a timeout/) + end + + defp timeout_exception?(_exception), do: false + + defp add_range_to_massive_blocks(range) do + clear_missing_ranges(range) + + range + |> Enum.to_list() + |> MassiveBlock.insert_block_numbers() + end + defp cap_seq(seq, errors) do {not_founds, other_errors} = Enum.split_with(errors, fn @@ -283,7 +317,7 @@ defmodule Indexer.Block.Catchup.Fetcher do |> Enum.map(&push_back(sequence, &1)) end - defp clear_missing_ranges(initial_range, errors) do + defp clear_missing_ranges(initial_range, errors \\ []) do success_numbers = Enum.to_list(initial_range) -- Enum.map(errors, &block_error_to_number/1) success_numbers diff --git a/apps/indexer/lib/indexer/block/catchup/helper.ex b/apps/indexer/lib/indexer/block/catchup/helper.ex deleted file mode 100644 index 951ed3548a46..000000000000 --- a/apps/indexer/lib/indexer/block/catchup/helper.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Indexer.Block.Catchup.Helper do - @moduledoc """ - Catchup helper functions - """ - - def sanitize_ranges(ranges) do - ranges - |> Enum.filter(&(not is_nil(&1))) - |> Enum.sort_by( - fn - from.._to -> from - el -> el - end, - :asc - ) - |> Enum.chunk_while( - nil, - fn - _from.._to = chunk, nil -> - {:cont, chunk} - - _ch_from..ch_to = chunk, acc_from..acc_to = acc -> - if Range.disjoint?(chunk, acc), - do: {:cont, acc, chunk}, - else: {:cont, acc_from..max(ch_to, acc_to)} - - num, nil -> - {:halt, num} - - num, acc_from.._ = acc -> - if Range.disjoint?(num..num, acc), do: {:cont, acc, num}, else: {:halt, acc_from} - - _, num -> - {:halt, num} - end, - fn reminder -> {:cont, reminder, nil} end - ) - end -end diff --git a/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex b/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex new file mode 100644 index 000000000000..fa1c91ed5ad7 --- /dev/null +++ b/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex @@ -0,0 +1,88 @@ +defmodule Indexer.Block.Catchup.MassiveBlocksFetcher do + @moduledoc """ + Fetches and indexes blocks by numbers from massive_blocks table. + """ + + use GenServer + + require Logger + + alias Explorer.Utility.MassiveBlock + alias Indexer.Block.Fetcher + + @increased_interval 10000 + + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_) do + send_new_task() + + {:ok, %{block_fetcher: generate_block_fetcher(), low_priority_blocks: []}} + end + + @impl true + def handle_info(:task, %{low_priority_blocks: low_priority_blocks} = state) do + {result, new_low_priority_blocks} = + case MassiveBlock.get_last_block_number(low_priority_blocks) do + nil -> + case low_priority_blocks do + [number | rest] -> + failed_blocks = process_block(state.block_fetcher, number) + {:processed, rest ++ failed_blocks} + + [] -> + {:empty, []} + end + + number -> + failed_blocks = process_block(state.block_fetcher, number) + {:processed, low_priority_blocks ++ failed_blocks} + end + + case result do + :processed -> send_new_task() + :empty -> send_new_task(@increased_interval) + end + + {:noreply, %{state | low_priority_blocks: new_low_priority_blocks}} + end + + def handle_info(_, state) do + {:noreply, state} + end + + defp process_block(block_fetcher, number) do + case Fetcher.fetch_and_import_range(block_fetcher, number..number, %{timeout: :infinity}) do + {:ok, _result} -> + Logger.info("MassiveBlockFetcher successfully proceed block #{inspect(number)}") + MassiveBlock.delete_block_number(number) + [] + + {:error, error} -> + Logger.error("MassiveBlockFetcher failed: #{inspect(error)}") + [number] + end + end + + defp generate_block_fetcher do + receipts_batch_size = Application.get_env(:indexer, :receipts_batch_size) + receipts_concurrency = Application.get_env(:indexer, :receipts_concurrency) + json_rpc_named_arguments = Application.get_env(:indexer, :json_rpc_named_arguments) + + %Fetcher{ + broadcast: :catchup, + callback_module: Indexer.Block.Catchup.Fetcher, + json_rpc_named_arguments: json_rpc_named_arguments, + receipts_batch_size: receipts_batch_size, + receipts_concurrency: receipts_concurrency + } + end + + defp send_new_task(interval \\ 0) do + Process.send_after(self(), :task, interval) + end +end diff --git a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex index 31d067217d8d..d4c19358eb29 100644 --- a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex +++ b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex @@ -5,11 +5,10 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do use GenServer - alias Explorer.{Chain, Repo} + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.{Chain, Helper, Repo} alias Explorer.Chain.Cache.BlockNumber - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Utility.{MissingBlockRange, MissingRangesManipulator} - alias Indexer.Block.Catchup.Helper @default_missing_ranges_batch_size 100_000 @future_check_interval Application.compile_env(:indexer, __MODULE__)[:future_check_interval] @@ -45,8 +44,6 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do end defp default_init do - MissingBlockRange.sanitize_missing_block_ranges() - {min_number, max_number} = get_initial_min_max() clear_to_bounds(min_number, max_number) @@ -148,14 +145,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do {:noreply, %{state | min_fetched_block_number: new_min_number}} else Process.send_after(self(), :update_past, @past_check_interval * 100) - {:noreply, %{state | min_fetched_block_number: reset_min_fetched_block_number(state.max_fetched_block_number)}} - end - end - - defp reset_min_fetched_block_number(max_fetched_block_number) do - case MissingBlockRange.fetch_min_max() do - %{min: nil} -> max_fetched_block_number - %{min: min} -> min + {:noreply, %{state | min_fetched_block_number: state.max_fetched_block_number}} end end @@ -233,7 +223,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do |> Enum.map(fn string_range -> case String.split(string_range, "..") do [from_string, "latest"] -> - ExplorerHelper.parse_integer(from_string) + Helper.parse_integer(from_string) [from_string, to_string] -> get_from_to(from_string, to_string) @@ -242,7 +232,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do nil end end) - |> Helper.sanitize_ranges() + |> RangesHelper.sanitize_ranges() case List.last(ranges) do _from.._to -> diff --git a/apps/indexer/lib/indexer/block/catchup/supervisor.ex b/apps/indexer/lib/indexer/block/catchup/supervisor.ex index c3388d103751..024c93c91a91 100644 --- a/apps/indexer/lib/indexer/block/catchup/supervisor.ex +++ b/apps/indexer/lib/indexer/block/catchup/supervisor.ex @@ -5,7 +5,7 @@ defmodule Indexer.Block.Catchup.Supervisor do use Supervisor - alias Indexer.Block.Catchup.{BoundIntervalSupervisor, MissingRangesCollector} + alias Indexer.Block.Catchup.{BoundIntervalSupervisor, MassiveBlocksFetcher, MissingRangesCollector} def child_spec([init_arguments]) do child_spec([init_arguments, []]) @@ -31,6 +31,7 @@ defmodule Indexer.Block.Catchup.Supervisor do [ {MissingRangesCollector, []}, {Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}, + {MassiveBlocksFetcher, []}, {BoundIntervalSupervisor, [bound_interval_supervisor_arguments, [name: BoundIntervalSupervisor]]} ], strategy: :one_for_one diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 5b5eaf0b8902..2676ab02cf78 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -16,11 +16,14 @@ defmodule Indexer.Block.Fetcher do alias Explorer.Chain.Cache.Blocks, as: BlocksCache alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions, Uncles} alias Indexer.Block.Fetcher.Receipts + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime + alias Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens, as: PolygonZkevmBridgeL1Tokens alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.{ + Beacon.Blob, BlockReward, - CoinBalance, ContractCode, InternalTransaction, ReplacedTransaction, @@ -33,7 +36,6 @@ defmodule Indexer.Block.Fetcher do alias Indexer.Transform.{ AddressCoinBalances, - AddressCoinBalancesDaily, Addresses, AddressTokenBalances, MintTransfers, @@ -42,9 +44,14 @@ defmodule Indexer.Block.Fetcher do TransactionActions } + alias Indexer.Transform.Optimism.Withdrawals, as: OptimismWithdrawals + alias Indexer.Transform.PolygonEdge.{DepositExecutes, Withdrawals} + alias Indexer.Transform.Shibarium.Bridge, as: ShibariumBridge + alias Indexer.Transform.Blocks, as: TransformBlocks + alias Indexer.Transform.PolygonZkevm.Bridge, as: PolygonZkevmBridge @type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()} @@ -111,7 +118,7 @@ defmodule Indexer.Block.Fetcher do end @decorate span(tracer: Tracer) - @spec fetch_and_import_range(t, Range.t()) :: + @spec fetch_and_import_range(t, Range.t(), map) :: {:ok, %{inserted: %{}, errors: [EthereumJSONRPC.Transport.error()]}} | {:error, {step :: atom(), reason :: [Ecto.Changeset.t()] | term()} @@ -122,7 +129,8 @@ defmodule Indexer.Block.Fetcher do callback_module: callback_module, json_rpc_named_arguments: json_rpc_named_arguments } = state, - _.._ = range + _.._ = range, + additional_options \\ %{} ) when callback_module != nil do {fetch_time, fetched_blocks} = @@ -144,6 +152,8 @@ defmodule Indexer.Block.Fetcher do %{token_transfers: token_transfers, tokens: tokens} = TokenTransfers.parse(logs), %{transaction_actions: transaction_actions} = TransactionActions.parse(logs), %{mint_transfers: mint_transfers} = MintTransfers.parse(logs), + optimism_withdrawals = + if(callback_module == Indexer.Block.Realtime.Fetcher, do: OptimismWithdrawals.parse(logs), else: []), polygon_edge_withdrawals = if(callback_module == Indexer.Block.Realtime.Fetcher, do: Withdrawals.parse(logs), else: []), polygon_edge_deposit_executes = @@ -151,6 +161,16 @@ defmodule Indexer.Block.Fetcher do do: DepositExecutes.parse(logs), else: [] ), + shibarium_bridge_operations = + if(callback_module == Indexer.Block.Realtime.Fetcher, + do: ShibariumBridge.parse(blocks, transactions_with_receipts, logs), + else: [] + ), + polygon_zkevm_bridge_operations = + if(callback_module == Indexer.Block.Realtime.Fetcher, + do: PolygonZkevmBridge.parse(blocks, logs), + else: [] + ), %FetchedBeneficiaries{params_set: beneficiary_params_set, errors: beneficiaries_errors} = fetch_beneficiaries(blocks, transactions_with_receipts, json_rpc_named_arguments), addresses = @@ -159,10 +179,12 @@ defmodule Indexer.Block.Fetcher do blocks: blocks, logs: logs, mint_transfers: mint_transfers, + shibarium_bridge_operations: shibarium_bridge_operations, token_transfers: token_transfers, transactions: transactions_with_receipts, transaction_actions: transaction_actions, - withdrawals: withdrawals_params + withdrawals: withdrawals_params, + polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations }), coin_balances_params_set = %{ @@ -173,12 +195,6 @@ defmodule Indexer.Block.Fetcher do withdrawals: withdrawals_params } |> AddressCoinBalances.params_set(), - coin_balances_params_daily_set = - %{ - coin_balances_params: coin_balances_params_set, - blocks: blocks - } - |> AddressCoinBalancesDaily.params_set(), beneficiaries_with_gas_payment = beneficiaries_with_gas_payment(blocks, beneficiary_params_set, transactions_with_receipts), address_token_balances = AddressTokenBalances.params_set(%{token_transfers_params: token_transfers}), @@ -188,7 +204,6 @@ defmodule Indexer.Block.Fetcher do basic_import_options = %{ addresses: %{params: addresses}, address_coin_balances: %{params: coin_balances_params_set}, - address_coin_balances_daily: %{params: coin_balances_params_daily_set}, address_token_balances: %{params: address_token_balances}, address_current_token_balances: %{ params: address_token_balances |> MapSet.to_list() |> TokenBalances.to_address_current_token_balances() @@ -198,23 +213,23 @@ defmodule Indexer.Block.Fetcher do block_rewards: %{errors: beneficiaries_errors, params: beneficiaries_with_gas_payment}, logs: %{params: logs}, token_transfers: %{params: token_transfers}, - tokens: %{on_conflict: :nothing, params: tokens}, + tokens: %{params: tokens}, transactions: %{params: transactions_with_receipts}, withdrawals: %{params: withdrawals_params}, token_instances: %{params: token_instances} }, - import_options = - (if Application.get_env(:explorer, :chain_type) == "polygon_edge" do - basic_import_options - |> Map.put_new(:polygon_edge_withdrawals, %{params: polygon_edge_withdrawals}) - |> Map.put_new(:polygon_edge_deposit_executes, %{params: polygon_edge_deposit_executes}) - else - basic_import_options - end), + chain_type_import_options = %{ + transactions_with_receipts: transactions_with_receipts, + optimism_withdrawals: optimism_withdrawals, + polygon_edge_withdrawals: polygon_edge_withdrawals, + polygon_edge_deposit_executes: polygon_edge_deposit_executes, + polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations, + shibarium_bridge_operations: shibarium_bridge_operations + }, {:ok, inserted} <- __MODULE__.import( state, - import_options + basic_import_options |> Map.merge(additional_options) |> import_options(chain_type_import_options) ), {:tx_actions, {:ok, inserted_tx_actions}} <- {:tx_actions, @@ -237,6 +252,43 @@ defmodule Indexer.Block.Fetcher do end end + defp import_options(basic_import_options, %{ + transactions_with_receipts: transactions_with_receipts, + optimism_withdrawals: optimism_withdrawals, + polygon_edge_withdrawals: polygon_edge_withdrawals, + polygon_edge_deposit_executes: polygon_edge_deposit_executes, + polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations, + shibarium_bridge_operations: shibarium_bridge_operations + }) do + case Application.get_env(:explorer, :chain_type) do + "ethereum" -> + basic_import_options + |> Map.put_new(:beacon_blob_transactions, %{ + params: transactions_with_receipts |> Enum.filter(&Map.has_key?(&1, :max_fee_per_blob_gas)) + }) + + "optimism" -> + basic_import_options + |> Map.put_new(:optimism_withdrawals, %{params: optimism_withdrawals}) + + "polygon_edge" -> + basic_import_options + |> Map.put_new(:polygon_edge_withdrawals, %{params: polygon_edge_withdrawals}) + |> Map.put_new(:polygon_edge_deposit_executes, %{params: polygon_edge_deposit_executes}) + + "polygon_zkevm" -> + basic_import_options + |> Map.put_new(:polygon_zkevm_bridge_operations, %{params: polygon_zkevm_bridge_operations}) + + "shibarium" -> + basic_import_options + |> Map.put_new(:shibarium_bridge_operations, %{params: shibarium_bridge_operations}) + + _ -> + basic_import_options + end + end + defp update_block_cache([]), do: :ok defp update_block_cache(blocks) when is_list(blocks) do @@ -302,6 +354,19 @@ defmodule Indexer.Block.Fetcher do def async_import_token_instances(_), do: :ok + def async_import_blobs(%{blocks: blocks}) do + timestamps = + blocks + |> Enum.filter(fn block -> block |> Map.get(:blob_gas_used, 0) > 0 end) + |> Enum.map(&Map.get(&1, :timestamp)) + + if !Enum.empty?(timestamps) do + Blob.async_fetch(timestamps) + end + end + + def async_import_blobs(_), do: :ok + def async_import_block_rewards([]), do: :ok def async_import_block_rewards(errors) when is_list(errors) do @@ -318,11 +383,17 @@ defmodule Indexer.Block.Fetcher do block_number = Map.fetch!(address_hash_to_block_number, to_string(address_hash)) %{address_hash: address_hash, block_number: block_number} end) - |> CoinBalance.async_fetch_balances() + |> CoinBalanceCatchup.async_fetch_balances() end def async_import_coin_balances(_, _), do: :ok + def async_import_realtime_coin_balances(%{address_coin_balances: balances}) do + CoinBalanceRealtime.async_fetch_balances(balances) + end + + def async_import_realtime_coin_balances(_), do: :ok + def async_import_created_contract_codes(%{transactions: transactions}) do transactions |> Enum.flat_map(fn @@ -384,6 +455,18 @@ defmodule Indexer.Block.Fetcher do def async_import_replaced_transactions(_), do: :ok + @doc """ + Fills a buffer of L1 token addresses to handle it asynchronously in + the Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens module. The addresses are + taken from the `operations` list. + """ + @spec async_import_polygon_zkevm_bridge_l1_tokens(map()) :: :ok + def async_import_polygon_zkevm_bridge_l1_tokens(%{polygon_zkevm_bridge_operations: operations}) do + PolygonZkevmBridgeL1Tokens.async_fetch(operations) + end + + def async_import_polygon_zkevm_bridge_l1_tokens(_), do: :ok + defp block_reward_errors_to_block_numbers(block_reward_errors) when is_list(block_reward_errors) do Enum.map(block_reward_errors, &block_reward_error_to_block_number/1) end @@ -415,15 +498,15 @@ defmodule Indexer.Block.Fetcher do def fetch_beneficiaries_manual(block, transactions) do block - |> Chain.block_reward_by_parts(transactions) + |> Block.block_reward_by_parts(transactions) |> reward_parts_to_beneficiaries() end defp reward_parts_to_beneficiaries(reward_parts) do reward = reward_parts.static_reward - |> Wei.sum(reward_parts.txn_fees) - |> Wei.sub(reward_parts.burned_fees) + |> Wei.sum(reward_parts.transaction_fees) + |> Wei.sub(reward_parts.burnt_fees) |> Wei.sum(reward_parts.uncle_reward) MapSet.new([ diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index b78688b85e18..7f1219ef2c96 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -9,10 +9,12 @@ defmodule Indexer.Block.Realtime.Fetcher do require Indexer.Tracer require Logger - import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] + import EthereumJSONRPC, only: [quantity_to_integer: 1] import Indexer.Block.Fetcher, only: [ + async_import_realtime_coin_balances: 1, + async_import_blobs: 1, async_import_block_rewards: 1, async_import_created_contract_codes: 1, async_import_internal_transactions: 1, @@ -21,22 +23,24 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_token_balances: 1, async_import_token_instances: 1, async_import_uncles: 1, + async_import_polygon_zkevm_bridge_l1_tokens: 1, fetch_and_import_range: 2 ] alias Ecto.Changeset - alias EthereumJSONRPC.{FetchedBalances, Subscription} + alias EthereumJSONRPC.Subscription alias Explorer.Chain - alias Explorer.Chain.Cache.Accounts alias Explorer.Chain.Events.Publisher alias Explorer.Counters.AverageBlockTime alias Explorer.Utility.MissingRangesManipulator alias Indexer.{Block, Tracer} alias Indexer.Block.Realtime.TaskSupervisor - alias Indexer.Fetcher.CoinBalance + alias Indexer.Fetcher.Optimism.TxnBatch, as: OptimismTxnBatch + alias Indexer.Fetcher.Optimism.Withdrawal, as: OptimismWithdrawal alias Indexer.Fetcher.PolygonEdge.{DepositExecute, Withdrawal} + alias Indexer.Fetcher.PolygonZkevm.BridgeL2, as: PolygonZkevmBridgeL2 + alias Indexer.Fetcher.Shibarium.L2, as: ShibariumBridgeL2 alias Indexer.Prometheus - alias Indexer.Transform.Addresses alias Timex.Duration @behaviour Block.Fetcher @@ -126,14 +130,18 @@ defmodule Indexer.Block.Realtime.Fetcher do new_previous_number = case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do {:ok, number} when is_nil(previous_number) or number != previous_number -> - if abnormal_gap?(number, previous_number) do - new_number = max(number, previous_number) - start_fetch_and_import(new_number, block_fetcher, previous_number) - new_number - else - start_fetch_and_import(number, block_fetcher, previous_number) - number - end + number = + if abnormal_gap?(number, previous_number) do + new_number = max(number, previous_number) + start_fetch_and_import(new_number, block_fetcher, previous_number) + new_number + else + start_fetch_and_import(number, block_fetcher, previous_number) + number + end + + fetch_validators_async() + number _ -> previous_number @@ -154,6 +162,16 @@ defmodule Indexer.Block.Realtime.Fetcher do {:noreply, state} end + if Application.compile_env(:explorer, :chain_type) == "stability" do + defp fetch_validators_async do + GenServer.cast(Indexer.Fetcher.Stability.Validator, :update_validators_list) + end + else + defp fetch_validators_async do + :ignore + end + end + defp subscribe_to_new_heads(%__MODULE__{subscription: nil} = state, subscribe_named_arguments) when is_list(subscribe_named_arguments) do case EthereumJSONRPC.subscribe("newHeads", subscribe_named_arguments) do @@ -191,47 +209,21 @@ defmodule Indexer.Block.Realtime.Fetcher do @import_options ~w(address_hash_to_fetched_balance_block_number)a @impl Block.Fetcher - def import( - block_fetcher, - %{ - address_coin_balances: %{params: address_coin_balances_params}, - address_coin_balances_daily: %{params: address_coin_balances_daily_params}, - address_hash_to_fetched_balance_block_number: address_hash_to_block_number, - addresses: %{params: addresses_params}, - block_rewards: block_rewards - } = options - ) do - with {:balances, - {:ok, - %{ - addresses_params: balances_addresses_params, - balances_params: balances_params, - balances_daily_params: balances_daily_params - }}} <- - {:balances, - balances(block_fetcher, %{ - address_hash_to_block_number: address_hash_to_block_number, - addresses_params: addresses_params, - balances_params: address_coin_balances_params, - balances_daily_params: address_coin_balances_daily_params - })}, - {block_reward_errors, chain_import_block_rewards} = Map.pop(block_rewards, :errors), - chain_import_options = - options - |> Map.drop(@import_options) - |> put_in([:addresses, :params], balances_addresses_params) - |> put_in([:blocks, :params, Access.all(), :consensus], true) - |> put_in([:block_rewards], chain_import_block_rewards) - |> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params) - |> put_in([Access.key(:address_coin_balances_daily, %{}), :params], balances_daily_params), - {:import, {:ok, imported} = ok} <- {:import, Chain.import(chain_import_options)} do + def import(_block_fetcher, %{block_rewards: block_rewards} = options) do + {block_reward_errors, chain_import_block_rewards} = Map.pop(block_rewards, :errors) + + chain_import_options = + options + |> Map.drop(@import_options) + |> put_in([:blocks, :params, Access.all(), :consensus], true) + |> put_in([:block_rewards], chain_import_block_rewards) + + with {:import, {:ok, imported} = ok} <- {:import, Chain.import(chain_import_options)} do async_import_remaining_block_data( imported, %{block_rewards: %{errors: block_reward_errors}} ) - Accounts.drop(imported[:addresses]) - ok end end @@ -288,9 +280,18 @@ defmodule Indexer.Block.Realtime.Fetcher do Indexer.Logger.metadata( fn -> if reorg? do + # we need to remove all rows from `op_transaction_batches` and `op_withdrawals` tables previously written starting from reorg block number + remove_optimism_assets_by_number(block_number_to_fetch) + # we need to remove all rows from `polygon_edge_withdrawals` and `polygon_edge_deposit_executes` tables previously written starting from reorg block number remove_polygon_edge_assets_by_number(block_number_to_fetch) + # we need to remove all rows from `shibarium_bridge` table previously written starting from reorg block number + remove_shibarium_assets_by_number(block_number_to_fetch) + + # we need to remove all rows from `polygon_zkevm_bridge` table previously written starting from reorg block number + remove_polygon_zkevm_assets_by_number(block_number_to_fetch) + # give previous fetch attempt (for same block number) a chance to finish # before fetching again, to reduce block consensus mistakes :timer.sleep(@reorg_delay) @@ -303,6 +304,13 @@ defmodule Indexer.Block.Realtime.Fetcher do ) end + defp remove_optimism_assets_by_number(block_number_to_fetch) do + if Application.get_env(:explorer, :chain_type) == "optimism" do + OptimismTxnBatch.handle_l2_reorg(block_number_to_fetch) + OptimismWithdrawal.remove(block_number_to_fetch) + end + end + defp remove_polygon_edge_assets_by_number(block_number_to_fetch) do if Application.get_env(:explorer, :chain_type) == "polygon_edge" do Withdrawal.remove(block_number_to_fetch) @@ -310,6 +318,18 @@ defmodule Indexer.Block.Realtime.Fetcher do end end + defp remove_polygon_zkevm_assets_by_number(block_number_to_fetch) do + if Application.get_env(:explorer, :chain_type) == "polygon_zkevm" do + PolygonZkevmBridgeL2.reorg_handle(block_number_to_fetch) + end + end + + defp remove_shibarium_assets_by_number(block_number_to_fetch) do + if Application.get_env(:explorer, :chain_type) == "shibarium" do + ShibariumBridgeL2.reorg_handle(block_number_to_fetch) + end + end + @decorate span(tracer: Tracer) defp do_fetch_and_import_block(block_number_to_fetch, block_fetcher, retry) do time_before = Timex.now() @@ -425,6 +445,7 @@ defmodule Indexer.Block.Realtime.Fetcher do imported, %{block_rewards: %{errors: block_reward_errors}} ) do + async_import_realtime_coin_balances(imported) async_import_block_rewards(block_reward_errors) async_import_created_contract_codes(imported) async_import_internal_transactions(imported) @@ -433,81 +454,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_token_instances(imported) async_import_uncles(imported) async_import_replaced_transactions(imported) - end - - defp balances( - %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}, - %{addresses_params: addresses_params} = options - ) do - case options - |> fetch_balances_params_list() - |> EthereumJSONRPC.fetch_balances(json_rpc_named_arguments, CoinBalance.batch_size()) do - {:ok, %FetchedBalances{params_list: params_list, errors: []}} -> - merged_addresses_params = - %{address_coin_balances: params_list} - |> Addresses.extract_addresses() - |> Kernel.++(addresses_params) - |> Addresses.merge_addresses() - - value_fetched_at = DateTime.utc_now() - - importable_balances_params = Enum.map(params_list, &Map.put(&1, :value_fetched_at, value_fetched_at)) - - block_timestamp_map = CoinBalance.block_timestamp_map(params_list, json_rpc_named_arguments) - - importable_balances_daily_params = - Enum.map(params_list, fn param -> - day = Map.get(block_timestamp_map, "#{param.block_number}") - (day && Map.put(param, :day, day)) || param - end) - - {:ok, - %{ - addresses_params: merged_addresses_params, - balances_params: importable_balances_params, - balances_daily_params: importable_balances_daily_params - }} - - {:error, _} = error -> - error - - {:ok, %FetchedBalances{errors: errors}} -> - {:error, errors} - end - end - - defp fetch_balances_params_list(%{ - addresses_params: addresses_params, - address_hash_to_block_number: address_hash_to_block_number, - balances_params: balances_params - }) do - addresses_params - |> addresses_params_to_fetched_balances_params_set(%{address_hash_to_block_number: address_hash_to_block_number}) - |> MapSet.union(balances_params_to_fetch_balances_params_set(balances_params)) - # stable order for easier moxing - |> Enum.sort_by(fn %{hash_data: hash_data, block_quantity: block_quantity} -> {hash_data, block_quantity} end) - end - - defp addresses_params_to_fetched_balances_params_set(addresses_params, %{ - address_hash_to_block_number: address_hash_to_block_number - }) do - Enum.into(addresses_params, MapSet.new(), fn %{hash: address_hash} = address_params when is_binary(address_hash) -> - block_number = - case address_params do - %{fetched_coin_balance_block_number: block_number} when is_integer(block_number) -> - block_number - - _ -> - Map.fetch!(address_hash_to_block_number, String.downcase(address_hash)) - end - - %{hash_data: address_hash, block_quantity: integer_to_quantity(block_number)} - end) - end - - defp balances_params_to_fetch_balances_params_set(balances_params) do - Enum.into(balances_params, MapSet.new(), fn %{address_hash: address_hash, block_number: block_number} -> - %{hash_data: address_hash, block_quantity: integer_to_quantity(block_number)} - end) + async_import_blobs(imported) + async_import_polygon_zkevm_bridge_l1_tokens(imported) end end diff --git a/apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex b/apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex new file mode 100644 index 000000000000..fabf22e75dcf --- /dev/null +++ b/apps/indexer/lib/indexer/bridged_tokens/calc_lp_tokens_total_liquidity.ex @@ -0,0 +1,52 @@ +defmodule Indexer.BridgedTokens.CalcLpTokensTotalLiquidity do + @moduledoc """ + Periodically updates LP tokens total liquidity + """ + + use GenServer + + require Logger + + alias Explorer.Chain.BridgedToken + + @interval :timer.minutes(20) + + def start_link([init_opts, gen_server_opts]) do + start_link(init_opts, gen_server_opts) + end + + def start_link(init_opts, gen_server_opts) do + GenServer.start_link(__MODULE__, init_opts, gen_server_opts) + end + + @impl GenServer + def init(opts) do + interval = opts[:interval] || @interval + + Process.send_after(self(), :calc_total_liquidity, interval) + + {:ok, %{interval: interval}} + end + + @impl GenServer + def handle_info(:calc_total_liquidity, %{interval: interval} = state) do + Logger.debug(fn -> "Calc LP tokens total liquidity" end) + + calc_total_liquidity() + + Process.send_after(self(), :calc_total_liquidity, interval) + + {:noreply, state} + end + + # don't handle other messages (e.g. :ssl_closed) + def handle_info(_, state) do + {:noreply, state} + end + + defp calc_total_liquidity do + BridgedToken.calc_lp_tokens_total_liquidity() + + Logger.debug(fn -> "Total liquidity fetched for LP tokens" end) + end +end diff --git a/apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex b/apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex new file mode 100644 index 000000000000..02fcaa970502 --- /dev/null +++ b/apps/indexer/lib/indexer/bridged_tokens/set_amb_bridged_metadata_for_tokens.ex @@ -0,0 +1,44 @@ +defmodule Indexer.BridgedTokens.SetAmbBridgedMetadataForTokens do + @moduledoc """ + Sets token metadata for bridged tokens from AMB extensions. + """ + + use GenServer + + require Logger + + alias Explorer.Chain.BridgedToken + + def start_link([init_opts, gen_server_opts]) do + start_link(init_opts, gen_server_opts) + end + + def start_link(init_opts, gen_server_opts) do + GenServer.start_link(__MODULE__, init_opts, gen_server_opts) + end + + @impl GenServer + def init(_opts) do + send(self(), :process_amb_tokens) + + {:ok, %{}} + end + + @impl GenServer + def handle_info(:process_amb_tokens, state) do + fetch_amb_bridged_tokens_metadata() + + {:noreply, state} + end + + # don't handle other messages (e.g. :ssl_closed) + def handle_info(_, state) do + {:noreply, state} + end + + defp fetch_amb_bridged_tokens_metadata do + :ok = BridgedToken.process_amb_tokens() + + Logger.debug(fn -> "Bridged status fetched for AMB tokens" end) + end +end diff --git a/apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex b/apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex new file mode 100644 index 000000000000..e6ba7afc3cb2 --- /dev/null +++ b/apps/indexer/lib/indexer/bridged_tokens/set_omni_bridged_metadata_for_tokens.ex @@ -0,0 +1,54 @@ +defmodule Indexer.BridgedTokens.SetOmniBridgedMetadataForTokens do + @moduledoc """ + Periodically checks unprocessed tokens and sets bridged status. + """ + + use GenServer + + require Logger + + alias Explorer.Chain.BridgedToken + + @interval :timer.minutes(20) + + def start_link([init_opts, gen_server_opts]) do + start_link(init_opts, gen_server_opts) + end + + def start_link(init_opts, gen_server_opts) do + GenServer.start_link(__MODULE__, init_opts, gen_server_opts) + end + + @impl GenServer + def init(opts) do + interval = opts[:interval] || @interval + + send(self(), :reveal_unprocessed_tokens) + + {:ok, %{interval: interval}} + end + + @impl GenServer + def handle_info(:reveal_unprocessed_tokens, %{interval: interval} = state) do + Logger.debug(fn -> "Reveal unprocessed tokens" end) + + {:ok, token_addresses} = BridgedToken.unprocessed_token_addresses_to_reveal_bridged_tokens() + + fetch_omni_bridged_tokens_metadata(token_addresses) + + Process.send_after(self(), :reveal_unprocessed_tokens, interval) + + {:noreply, state} + end + + # don't handle other messages (e.g. :ssl_closed) + def handle_info(_, state) do + {:noreply, state} + end + + defp fetch_omni_bridged_tokens_metadata(token_addresses) do + :ok = BridgedToken.fetch_omni_bridged_tokens_metadata(token_addresses) + + Logger.debug(fn -> "Bridged status fetched for tokens" end) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/blob.ex b/apps/indexer/lib/indexer/fetcher/beacon/blob.ex new file mode 100644 index 000000000000..d35a680a7d13 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/beacon/blob.ex @@ -0,0 +1,174 @@ +defmodule Indexer.Fetcher.Beacon.Blob do + @moduledoc """ + Fills beacon_blobs DB table. + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + require Logger + + alias Explorer.Repo + alias Explorer.Chain.Beacon.{Blob, Reader} + alias Explorer.Chain.Data + alias Indexer.{BufferedTask, Tracer} + alias Indexer.Fetcher.Beacon.Blob.Supervisor, as: BlobSupervisor + alias Indexer.Fetcher.Beacon.Client + + @behaviour BufferedTask + + @default_max_batch_size 10 + @default_max_concurrency 1 + @default_retries_limit 2 + @default_retry_deadline :timer.minutes(5) + + @doc """ + Asynchronously fetches blobs for given `block_timestamp`. + """ + def async_fetch(block_timestamps) do + if BlobSupervisor.disabled?() do + :ok + else + BufferedTask.buffer(__MODULE__, block_timestamps |> Enum.map(&entry/1)) + end + end + + @spec child_spec([...]) :: %{ + :id => any(), + :start => {atom(), atom(), list()}, + optional(:modules) => :dynamic | [atom()], + optional(:restart) => :permanent | :temporary | :transient, + optional(:shutdown) => :brutal_kill | :infinity | non_neg_integer(), + optional(:significant) => boolean(), + optional(:type) => :supervisor | :worker + } + @doc false + def child_spec([init_options, gen_server_options]) do + state = + :indexer + |> Application.get_env(__MODULE__) + |> Keyword.take([:start_block, :end_block, :reference_slot, :reference_timestamp, :slot_duration]) + |> Enum.into(%{}) + + merged_init_options = + defaults() + |> Keyword.merge(init_options) + |> Keyword.put(:state, state) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, state) do + {:ok, final} = + Reader.stream_missed_blob_transactions_timestamps( + initial, + fn fields, acc -> + fields + |> entry() + |> reducer.(acc) + end, + state.start_block, + state.end_block + ) + + final + end + + @impl BufferedTask + @decorate trace( + name: "fetch", + resource: "Indexer.Fetcher.Beacon.Blob.run/2", + service: :indexer, + tracer: Tracer + ) + def run(entries, state) do + entry_count = Enum.count(entries) + Logger.metadata(count: entry_count) + + Logger.debug(fn -> "fetching" end) + + entries + |> Enum.map(&entry_to_slot(&1, state)) + |> Client.get_blob_sidecars() + |> case do + {:ok, fetched_blobs, retry_indices} -> + run_fetched_blobs(fetched_blobs) + + retry_entities = + retry_indices + |> Enum.map(&Enum.at(entries, &1)) + |> Enum.filter(&should_retry?/1) + |> Enum.map(&increment_retry_count/1) + + if Enum.empty?(retry_entities) do + :ok + else + {:retry, retry_entities} + end + end + end + + defp entry(block_timestamp) do + {DateTime.to_unix(block_timestamp), 0} + end + + defp increment_retry_count({block_timestamp, retry_count}), do: {block_timestamp, retry_count + 1} + + # Stop retrying after 2 failed retries for slots older than 5 minutes + defp should_retry?({block_timestamp, retry_count}), + do: + retry_count < @default_retries_limit || + block_timestamp + @default_retry_deadline > DateTime.to_unix(DateTime.utc_now()) + + defp entry_to_slot({block_timestamp, _}, state), do: timestamp_to_slot(block_timestamp, state) + + @doc """ + Converts block timestamp to the slot number. + """ + @spec timestamp_to_slot(non_neg_integer(), map()) :: non_neg_integer() + def timestamp_to_slot(block_timestamp, %{ + reference_timestamp: reference_timestamp, + reference_slot: reference_slot, + slot_duration: slot_duration + }) do + ((block_timestamp - reference_timestamp) |> div(slot_duration)) + reference_slot + end + + defp run_fetched_blobs(fetched_blobs) do + blobs = + fetched_blobs + |> Enum.flat_map(fn %{"data" => blobs} -> blobs end) + |> Enum.map(&blob_entry/1) + + Repo.insert_all(Blob, blobs, on_conflict: :nothing, conflict_target: [:hash]) + end + + defp blob_entry(%{ + "blob" => blob, + "kzg_commitment" => kzg_commitment, + "kzg_proof" => kzg_proof + }) do + {:ok, kzg_commitment} = Data.cast(kzg_commitment) + {:ok, blob} = Data.cast(blob) + {:ok, kzg_proof} = Data.cast(kzg_proof) + + %{ + hash: Blob.hash(kzg_commitment.bytes), + blob_data: blob, + kzg_commitment: kzg_commitment, + kzg_proof: kzg_proof + } + end + + defp defaults do + [ + poll: false, + flush_interval: :timer.seconds(3), + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + task_supervisor: Indexer.Fetcher.Beacon.Blob.TaskSupervisor, + metadata: [fetcher: :beacon_blob] + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/client.ex b/apps/indexer/lib/indexer/fetcher/beacon/client.ex new file mode 100644 index 000000000000..8d5b79b63f11 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/beacon/client.ex @@ -0,0 +1,88 @@ +defmodule Indexer.Fetcher.Beacon.Client do + @moduledoc """ + HTTP Client for Beacon Chain RPC + """ + alias HTTPoison.Response + require Logger + + @request_error_msg "Error while sending request to beacon rpc" + + def http_get_request(url) do + case Application.get_env(:explorer, :http_adapter).get(url) do + {:ok, %Response{body: body, status_code: 200}} -> + Jason.decode(body) + + {:ok, %Response{body: body, status_code: _}} -> + {:error, body} + + {:error, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to beacon rpc: #{url}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, @request_error_msg} + end + end + + @doc """ + Fetches blob sidecars for multiple given beacon `slots` from the beacon RPC. + + Returns `{:ok, blob_sidecars_list, retry_indices_list}` + where `retry_indices_list` is the list of indices from `slots` for which the request failed and should be retried. + """ + @spec get_blob_sidecars([integer()]) :: {:ok, list(), [integer()]} + def get_blob_sidecars([]), do: {:ok, [], []} + + def get_blob_sidecars(slots) when is_list(slots) do + {oks, errors_with_retries} = + slots + |> Enum.map(&get_blob_sidecars/1) + |> Enum.with_index() + |> Enum.map(&first_if_ok/1) + |> Enum.split_with(&successful?/1) + + {errors, retries} = errors_with_retries |> Enum.unzip() + + if !Enum.empty?(errors) do + Logger.error(fn -> + [ + "Errors while fetching blob sidecars (failed for #{Enum.count(errors)}/#{Enum.count(slots)}) from beacon rpc: ", + inspect(Enum.take(errors, 3), limit: :infinity, printable_limit: :infinity) + ] + end) + end + + {:ok, oks |> Enum.map(fn {_, blob} -> blob end), retries} + end + + @spec get_blob_sidecars(integer()) :: {:error, any()} | {:ok, any()} + def get_blob_sidecars(slot) do + http_get_request(blob_sidecars_url(slot)) + end + + defp first_if_ok({{:ok, _} = first, _}), do: first + defp first_if_ok(res), do: res + + defp successful?({:ok, _}), do: true + defp successful?(_), do: false + + @spec get_header(integer()) :: {:error, any()} | {:ok, any()} + def get_header(slot) do + http_get_request(header_url(slot)) + end + + def blob_sidecars_url(slot), do: "#{base_url()}" <> "/eth/v1/beacon/blob_sidecars/" <> to_string(slot) + + def header_url(slot), do: "#{base_url()}" <> "/eth/v1/beacon/headers/" <> to_string(slot) + + def base_url do + Application.get_env(:indexer, Indexer.Fetcher.Beacon)[:beacon_rpc] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/block_reward.ex b/apps/indexer/lib/indexer/fetcher/block_reward.ex index f6825a5ffce6..4928cef1c811 100644 --- a/apps/indexer/lib/indexer/fetcher/block_reward.ex +++ b/apps/indexer/lib/indexer/fetcher/block_reward.ex @@ -20,8 +20,8 @@ defmodule Indexer.Fetcher.BlockReward do alias Explorer.Chain.Cache.Accounts alias Indexer.{BufferedTask, Tracer} alias Indexer.Fetcher.BlockReward.Supervisor, as: BlockRewardSupervisor - alias Indexer.Fetcher.CoinBalance - alias Indexer.Transform.{AddressCoinBalances, AddressCoinBalancesDaily, Addresses} + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Indexer.Transform.{AddressCoinBalances, Addresses} @behaviour BufferedTask @@ -133,7 +133,7 @@ defmodule Indexer.Fetcher.BlockReward do {:ok, %{address_coin_balances: address_coin_balances, addresses: addresses}} -> Accounts.drop(addresses) - CoinBalance.async_fetch_balances(address_coin_balances) + CoinBalanceCatchup.async_fetch_balances(address_coin_balances) retry_errors(errors) @@ -274,28 +274,9 @@ defmodule Indexer.Fetcher.BlockReward do addresses_params = Addresses.extract_addresses(%{block_reward_contract_beneficiaries: block_rewards_params}) address_coin_balances_params_set = AddressCoinBalances.params_set(%{beneficiary_params: block_rewards_params}) - address_coin_balances_params_with_block_timestamp = - block_rewards_params - |> Enum.map(fn block_rewards_param -> - %{ - address_hash: block_rewards_param.address_hash, - block_number: block_rewards_param.block_number, - block_timestamp: block_rewards_param.block_timestamp - } - end) - |> Enum.into(MapSet.new()) - - address_coin_balances_params_with_block_timestamp_set = %{ - address_coin_balances_params_with_block_timestamp: address_coin_balances_params_with_block_timestamp - } - - address_coin_balances_daily_params_set = - AddressCoinBalancesDaily.params_set(address_coin_balances_params_with_block_timestamp_set) - Chain.import(%{ addresses: %{params: addresses_params}, address_coin_balances: %{params: address_coin_balances_params_set}, - address_coin_balances_daily: %{params: address_coin_balances_daily_params_set}, block_rewards: %{params: block_rewards_params} }) end @@ -344,7 +325,7 @@ defmodule Indexer.Fetcher.BlockReward do defp fetched_beneficiary_error_to_iodata(%{code: code, message: message, data: %{block_quantity: block_quantity}}) when is_integer(code) and is_binary(message) and is_binary(block_quantity) do - ["@", quantity_to_integer(block_quantity), ": (", to_string(code), ") ", message, ?\n] + ["@", block_quantity |> quantity_to_integer() |> to_string(), ": (", to_string(code), ") ", message, ?\n] end defp defaults do diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex new file mode 100644 index 000000000000..ee4e93d93931 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex @@ -0,0 +1,77 @@ +defmodule Indexer.Fetcher.CoinBalance.Catchup do + @moduledoc """ + Fetches `t:Explorer.Chain.Address.CoinBalance.t/0` and updates `t:Explorer.Chain.Address.t/0` `fetched_coin_balance` and + `fetched_coin_balance_block_number` to value at max `t:Explorer.Chain.Address.CoinBalance.t/0` `block_number` for the given `t:Explorer.Chain.Address.t/` `hash`. + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.Chain + alias Explorer.Chain.{Block, Hash} + alias Indexer.{BufferedTask, Tracer} + alias Indexer.Fetcher.CoinBalance.Catchup.Supervisor, as: CoinBalanceSupervisor + alias Indexer.Fetcher.CoinBalance.Helper + + @behaviour BufferedTask + + @default_max_batch_size 500 + @default_max_concurrency 4 + + @doc """ + Asynchronously fetches balances for each address `hash` at the `block_number`. + """ + @spec async_fetch_balances([ + %{required(:address_hash) => Hash.Address.t(), required(:block_number) => Block.block_number()} + ]) :: :ok + def async_fetch_balances(balance_fields) when is_list(balance_fields) do + if CoinBalanceSupervisor.disabled?() do + :ok + else + entries = Enum.map(balance_fields, &Helper.entry/1) + + BufferedTask.buffer(__MODULE__, entries) + end + end + + def child_spec(params) do + Helper.child_spec(params, defaults(), __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, _) do + {:ok, final} = + Chain.stream_unfetched_balances( + initial, + fn address_fields, acc -> + address_fields + |> Helper.entry() + |> reducer.(acc) + end, + true + ) + + final + end + + @impl BufferedTask + @decorate trace( + name: "fetch", + resource: "Indexer.Fetcher.CoinBalance.Catchup.run/2", + service: :indexer, + tracer: Tracer + ) + def run(entries, json_rpc_named_arguments) do + Helper.run(entries, json_rpc_named_arguments, true) + end + + defp defaults do + [ + flush_interval: :timer.seconds(3), + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + task_supervisor: Indexer.Fetcher.CoinBalance.Catchup.TaskSupervisor, + metadata: [fetcher: :coin_balance_catchup] + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex similarity index 71% rename from apps/indexer/lib/indexer/fetcher/coin_balance.ex rename to apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex index 18de40eaaf16..e0b012f2adef 100644 --- a/apps/indexer/lib/indexer/fetcher/coin_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex @@ -1,95 +1,48 @@ -defmodule Indexer.Fetcher.CoinBalance do +defmodule Indexer.Fetcher.CoinBalance.Helper do @moduledoc """ - Fetches `t:Explorer.Chain.Address.CoinBalance.t/0` and updates `t:Explorer.Chain.Address.t/0` `fetched_coin_balance` and - `fetched_coin_balance_block_number` to value at max `t:Explorer.Chain.Address.CoinBalance.t/0` `block_number` for the given `t:Explorer.Chain.Address.t/` `hash`. + Common functions for `Indexer.Fetcher.CoinBalance.Catchup` and `Indexer.Fetcher.CoinBalance.Realtime` modules """ - use Indexer.Fetcher, restart: :permanent - use Spandex.Decorators + import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] require Logger - import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1] - - alias EthereumJSONRPC.{Blocks, FetchedBalances} + alias EthereumJSONRPC.{Blocks, FetchedBalances, Utility.RangesHelper} alias Explorer.Chain - alias Explorer.Chain.{Block, Hash} alias Explorer.Chain.Cache.Accounts - alias Indexer.{BufferedTask, Tracer} - alias Indexer.Fetcher.CoinBalance.Supervisor, as: CoinBalanceSupervisor - - @behaviour BufferedTask - - @default_max_batch_size 500 - @default_max_concurrency 4 - - def batch_size, do: defaults()[:max_batch_size] - - @doc """ - Asynchronously fetches balances for each address `hash` at the `block_number`. - """ - @spec async_fetch_balances([ - %{required(:address_hash) => Hash.Address.t(), required(:block_number) => Block.block_number()} - ]) :: :ok - def async_fetch_balances(balance_fields) when is_list(balance_fields) do - if CoinBalanceSupervisor.disabled?() do - :ok - else - entries = Enum.map(balance_fields, &entry/1) - - BufferedTask.buffer(__MODULE__, entries) - end - end + alias Explorer.Chain.Hash + alias Indexer.BufferedTask @doc false # credo:disable-for-next-line Credo.Check.Design.DuplicatedCode - def child_spec([init_options, gen_server_options]) do + def child_spec([init_options, gen_server_options], defaults, module) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) unless state do raise ArgumentError, - ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> + ":json_rpc_named_arguments must be provided to `#{module}.child_spec " <> "to allow for json_rpc calls when running." end merged_init_options = - defaults() + defaults |> Keyword.merge(mergeable_init_options) |> Keyword.put(:state, state) - Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__) - end - - @impl BufferedTask - def init(initial, reducer, _) do - {:ok, final} = - Chain.stream_unfetched_balances( - initial, - fn address_fields, acc -> - address_fields - |> entry() - |> reducer.(acc) - end, - true - ) - - final + Supervisor.child_spec({BufferedTask, [{module, merged_init_options}, gen_server_options]}, id: module) end - @impl BufferedTask - @decorate trace(name: "fetch", resource: "Indexer.Fetcher.CoinBalance.run/2", service: :indexer, tracer: Tracer) - def run(entries, json_rpc_named_arguments) do + def run(entries, json_rpc_named_arguments, filter_non_traceable_blocks? \\ true) do # the same address may be used more than once in the same block, but we only want one `Balance` for a given # `{address, block}`, so take unique params only unique_entries = Enum.uniq(entries) - min_block = Application.get_env(:indexer, :trace_first_block) - max_block = Application.get_env(:indexer, :trace_last_block) - unique_filtered_entries = - Enum.filter(unique_entries, fn {_hash, block_number} -> - block_number >= min_block && if max_block, do: block_number <= max_block, else: true - end) + if filter_non_traceable_blocks? do + Enum.filter(unique_entries, fn {_hash, block_number} -> RangesHelper.traceable_block_number?(block_number) end) + else + unique_entries + end unique_entry_count = Enum.count(unique_filtered_entries) Logger.metadata(count: unique_entry_count) @@ -115,15 +68,15 @@ defmodule Indexer.Fetcher.CoinBalance do end end + def entry(%{address_hash: %Hash{bytes: address_hash_bytes}, block_number: block_number}) do + {address_hash_bytes, block_number} + end + defp entry_to_params({address_hash_bytes, block_number}) when is_integer(block_number) do {:ok, address_hash} = Hash.Address.cast(address_hash_bytes) %{block_quantity: integer_to_quantity(block_number), hash_data: to_string(address_hash)} end - defp entry(%{address_hash: %Hash{bytes: address_hash_bytes}, block_number: block_number}) do - {address_hash_bytes, block_number} - end - # We want to record all historical balances for an address, but have the address itself have balance from the # `Balance` with the greatest block_number for that address. def balances_params_to_address_params(balances_params) do @@ -268,14 +221,4 @@ defmodule Indexer.Fetcher.CoinBalance do end end) end - - defp defaults do - [ - flush_interval: :timer.seconds(3), - max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, - max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, - task_supervisor: Indexer.Fetcher.CoinBalance.TaskSupervisor, - metadata: [fetcher: :coin_balance] - ] - end end diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex new file mode 100644 index 000000000000..ba93a5c790fb --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex @@ -0,0 +1,60 @@ +defmodule Indexer.Fetcher.CoinBalance.Realtime do + @moduledoc """ + Separate version of `Indexer.Fetcher.CoinBalance.Catchup` for fetching balances from realtime block fetcher + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.Chain.{Block, Hash} + alias Indexer.{BufferedTask, Tracer} + alias Indexer.Fetcher.CoinBalance.Helper + + @behaviour BufferedTask + + @default_max_batch_size 500 + @default_max_concurrency 4 + + @doc """ + Asynchronously fetches balances for each address `hash` at the `block_number`. + """ + @spec async_fetch_balances([ + %{required(:address_hash) => Hash.Address.t(), required(:block_number) => Block.block_number()} + ]) :: :ok + def async_fetch_balances(balance_fields) when is_list(balance_fields) do + entries = Enum.map(balance_fields, &Helper.entry/1) + + BufferedTask.buffer(__MODULE__, entries) + end + + def child_spec(params) do + Helper.child_spec(params, defaults(), __MODULE__) + end + + @impl BufferedTask + def init(_, _, _) do + {0, []} + end + + @impl BufferedTask + @decorate trace( + name: "fetch", + resource: "Indexer.Fetcher.CoinBalance.Realtime.run/2", + service: :indexer, + tracer: Tracer + ) + def run(entries, json_rpc_named_arguments) do + Helper.run(entries, json_rpc_named_arguments, false) + end + + defp defaults do + [ + poll: false, + flush_interval: :timer.seconds(3), + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + task_supervisor: Indexer.Fetcher.CoinBalance.Realtime.TaskSupervisor, + metadata: [fetcher: :coin_balance_realtime] + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex b/apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex index a4a5f5f97d6f..e56210292770 100644 --- a/apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex +++ b/apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex @@ -19,7 +19,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do alias Explorer.Chain.Address.{CoinBalance, CoinBalanceDaily} alias Explorer.Chain.Cache.{Accounts, BlockNumber} alias Explorer.Counters.AverageBlockTime - alias Indexer.Fetcher.CoinBalance, as: CoinBalanceFetcher + alias Indexer.Fetcher.CoinBalance.Helper, as: CoinBalanceHelper alias Timex.Duration @type block_number :: integer @@ -205,7 +205,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do :ok {:ok, %{params_list: params_list}} -> - address_params = CoinBalanceFetcher.balances_params_to_address_params(params_list) + address_params = CoinBalanceHelper.balances_params_to_address_params(params_list) Chain.import(%{ addresses: %{params: address_params, with: :balance_changeset}, @@ -224,14 +224,14 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do end defp do_import(%FetchedBalances{} = fetched_balances) do - case CoinBalanceFetcher.import_fetched_balances(fetched_balances, :on_demand) do + case CoinBalanceHelper.import_fetched_balances(fetched_balances, :on_demand) do {:ok, %{addresses: [address]}} -> {:ok, address} _ -> :error end end defp do_import_daily_balances(%FetchedBalances{} = fetched_balances) do - case CoinBalanceFetcher.import_fetched_daily_balances(fetched_balances, :on_demand) do + case CoinBalanceHelper.import_fetched_daily_balances(fetched_balances, :on_demand) do {:ok, %{addresses: [address]}} -> {:ok, address} _ -> :error end diff --git a/apps/indexer/lib/indexer/fetcher/contract_code.ex b/apps/indexer/lib/indexer/fetcher/contract_code.ex index c9f8f0f7b6e8..891b52676fe9 100644 --- a/apps/indexer/lib/indexer/fetcher/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/contract_code.ex @@ -14,7 +14,7 @@ defmodule Indexer.Fetcher.ContractCode do alias Explorer.Chain.{Block, Hash} alias Explorer.Chain.Cache.Accounts alias Indexer.{BufferedTask, Tracer} - alias Indexer.Fetcher.CoinBalance, as: CoinBalanceFetcher + alias Indexer.Fetcher.CoinBalance.Helper, as: CoinBalanceHelper alias Indexer.Transform.Addresses @behaviour BufferedTask @@ -98,6 +98,7 @@ defmodule Indexer.Fetcher.ContractCode do Logger.debug("fetching created_contract_code for transactions") entries + |> Enum.uniq() |> Enum.map(¶ms/1) |> EthereumJSONRPC.fetch_codes(json_rpc_named_arguments) |> case do @@ -121,7 +122,7 @@ defmodule Indexer.Fetcher.ContractCode do |> EthereumJSONRPC.fetch_balances(json_rpc_named_arguments) |> case do {:ok, fetched_balances} -> - balance_addresses_params = CoinBalanceFetcher.balances_params_to_address_params(fetched_balances.params_list) + balance_addresses_params = CoinBalanceHelper.balances_params_to_address_params(fetched_balances.params_list) merged_addresses_params = Addresses.merge_addresses(addresses_params ++ balance_addresses_params) diff --git a/apps/indexer/lib/indexer/fetcher/first_trace_on_demand.ex b/apps/indexer/lib/indexer/fetcher/first_trace_on_demand.ex index 10bbc181ddc8..ed8f87a2881a 100644 --- a/apps/indexer/lib/indexer/fetcher/first_trace_on_demand.ex +++ b/apps/indexer/lib/indexer/fetcher/first_trace_on_demand.ex @@ -60,6 +60,9 @@ defmodule Indexer.Fetcher.FirstTraceOnDemand do end @impl true + # Don't fetch first trace for pending transactions + def handle_cast({:fetch, %{block_hash: nil}}, state), do: {:noreply, state} + def handle_cast({:fetch, transaction}, state) do fetch_first_trace(transaction, state) diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index 7fefb5b4400b..8bca28581e7f 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -12,6 +12,7 @@ defmodule Indexer.Fetcher.InternalTransaction do import Indexer.Block.Fetcher, only: [async_import_coin_balances: 2] + alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Chain alias Explorer.Chain.Block alias Explorer.Chain.Cache.{Accounts, Blocks} @@ -100,7 +101,7 @@ defmodule Indexer.Fetcher.InternalTransaction do filtered_unique_numbers = unique_numbers - |> EthereumJSONRPC.are_block_numbers_in_range?() + |> RangesHelper.filter_traceable_block_numbers() |> drop_genesis(json_rpc_named_arguments) filtered_unique_numbers_count = Enum.count(filtered_unique_numbers) @@ -110,19 +111,7 @@ defmodule Indexer.Fetcher.InternalTransaction do json_rpc_named_arguments |> Keyword.fetch!(:variant) - |> case do - variant - when variant in [EthereumJSONRPC.Nethermind, EthereumJSONRPC.Erigon, EthereumJSONRPC.Besu, EthereumJSONRPC.RSK] -> - EthereumJSONRPC.fetch_block_internal_transactions(filtered_unique_numbers, json_rpc_named_arguments) - - _ -> - try do - fetch_block_internal_transactions_by_transactions(filtered_unique_numbers, json_rpc_named_arguments) - rescue - error -> - {:error, error, __STACKTRACE__} - end - end + |> fetch_internal_transactions(filtered_unique_numbers, json_rpc_named_arguments) |> case do {:ok, internal_transactions_params} -> safe_import_internal_transaction(internal_transactions_params, filtered_unique_numbers) @@ -158,6 +147,34 @@ defmodule Indexer.Fetcher.InternalTransaction do end end + defp fetch_internal_transactions(variant, block_numbers, json_rpc_named_arguments) do + if variant in block_traceable_variants() do + EthereumJSONRPC.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) + else + try do + fetch_block_internal_transactions_by_transactions(block_numbers, json_rpc_named_arguments) + rescue + error -> + {:error, error, __STACKTRACE__} + end + end + end + + @default_block_traceable_variants [ + EthereumJSONRPC.Nethermind, + EthereumJSONRPC.Erigon, + EthereumJSONRPC.Besu, + EthereumJSONRPC.RSK, + EthereumJSONRPC.Filecoin + ] + defp block_traceable_variants do + if Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth)[:block_traceable?] do + [EthereumJSONRPC.Geth | @default_block_traceable_variants] + else + @default_block_traceable_variants + end + end + defp drop_genesis(block_numbers, json_rpc_named_arguments) do first_block = Application.get_env(:indexer, :trace_first_block) @@ -197,6 +214,7 @@ defmodule Indexer.Fetcher.InternalTransaction do block_number, {:ok, acc_list} -> block_number |> Chain.get_transactions_of_block_number() + |> filter_non_traceable_transactions() |> Enum.map(¶ms(&1)) |> case do [] -> @@ -220,6 +238,14 @@ defmodule Indexer.Fetcher.InternalTransaction do end) end + @zetachain_non_traceable_type 88 + defp filter_non_traceable_transactions(transactions) do + case Application.get_env(:explorer, :chain_type) do + "zetachain" -> Enum.reject(transactions, &(&1.type == @zetachain_non_traceable_type)) + _ -> transactions + end + end + defp safe_import_internal_transaction(internal_transactions_params, block_numbers) do import_internal_transaction(internal_transactions_params, block_numbers) rescue @@ -229,11 +255,11 @@ defmodule Indexer.Fetcher.InternalTransaction do end defp import_internal_transaction(internal_transactions_params, unique_numbers) do - internal_transactions_params_without_failed_creations = remove_failed_creations(internal_transactions_params) + internal_transactions_params_marked = mark_failed_transactions(internal_transactions_params) addresses_params = Addresses.extract_addresses(%{ - internal_transactions: internal_transactions_params_without_failed_creations + internal_transactions: internal_transactions_params_marked }) address_hash_to_block_number = @@ -244,11 +270,10 @@ defmodule Indexer.Fetcher.InternalTransaction do empty_block_numbers = unique_numbers |> MapSet.new() - |> MapSet.difference(MapSet.new(internal_transactions_params_without_failed_creations, & &1.block_number)) + |> MapSet.difference(MapSet.new(internal_transactions_params_marked, & &1.block_number)) |> Enum.map(&%{block_number: &1}) - internal_transactions_and_empty_block_numbers = - internal_transactions_params_without_failed_creations ++ empty_block_numbers + internal_transactions_and_empty_block_numbers = internal_transactions_params_marked ++ empty_block_numbers imports = Chain.import(%{ @@ -285,34 +310,42 @@ defmodule Indexer.Fetcher.InternalTransaction do end end - defp remove_failed_creations(internal_transactions_params) do + defp mark_failed_transactions(internal_transactions_params) do + # we store reversed trace addresses for more efficient list head-tail decomposition in has_failed_parent? + failed_parent_paths = + internal_transactions_params + |> Enum.filter(& &1[:error]) + |> Enum.map(&Enum.reverse([&1.transaction_hash | &1.trace_address])) + |> MapSet.new() + internal_transactions_params |> Enum.map(fn internal_transaction_param -> - transaction_index = internal_transaction_param[:transaction_index] - block_number = internal_transaction_param[:block_number] - - failed_parent = - internal_transactions_params - |> Enum.filter(fn internal_transactions_param -> - internal_transactions_param[:block_number] == block_number && - internal_transactions_param[:transaction_index] == transaction_index && - internal_transactions_param[:trace_address] == [] && !is_nil(internal_transactions_param[:error]) - end) - |> Enum.at(0) - - if failed_parent do + if has_failed_parent?( + failed_parent_paths, + internal_transaction_param.trace_address, + [internal_transaction_param.transaction_hash] + ) do + # TODO: consider keeping these deleted fields in the reverted transactions internal_transaction_param |> Map.delete(:created_contract_address_hash) |> Map.delete(:created_contract_code) |> Map.delete(:gas_used) |> Map.delete(:output) - |> Map.put_new(:error, failed_parent[:error]) + |> Map.put(:error, internal_transaction_param[:error] || "Parent reverted") else internal_transaction_param end end) end + defp has_failed_parent?(failed_parent_paths, [head | tail], reverse_path_acc) do + MapSet.member?(failed_parent_paths, reverse_path_acc) or + has_failed_parent?(failed_parent_paths, tail, [head | reverse_path_acc]) + end + + # don't count itself as a parent + defp has_failed_parent?(_failed_parent_paths, [], _reverse_path_acc), do: false + defp handle_unique_key_violation(%{exception: %{postgres: %{code: :unique_violation}}}, block_numbers) do BlocksRunner.invalidate_consensus_blocks(block_numbers) @@ -363,8 +396,9 @@ defmodule Indexer.Fetcher.InternalTransaction do defp invalidate_block_from_error(_error_data), do: :ok - defp defaults do + def defaults do [ + poll: false, flush_interval: :timer.seconds(3), max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, diff --git a/apps/indexer/lib/indexer/fetcher/optimism.ex b/apps/indexer/lib/indexer/fetcher/optimism.ex new file mode 100644 index 000000000000..cd367d59f466 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism.ex @@ -0,0 +1,310 @@ +defmodule Indexer.Fetcher.Optimism do + @moduledoc """ + Contains common functions for Optimism* fetchers. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import EthereumJSONRPC, + only: [ + fetch_block_number_by_tag_op_version: 2, + json_rpc: 2, + integer_to_quantity: 1, + quantity_to_integer: 1, + request: 1 + ] + + import Explorer.Helper, only: [parse_integer: 1] + + alias EthereumJSONRPC.Block.ByNumber + alias Indexer.Helper + + @fetcher_name :optimism + @block_check_interval_range_size 100 + @eth_get_logs_range_size 1000 + @finite_retries_number 3 + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + Logger.metadata(fetcher: @fetcher_name) + :ignore + end + + @doc """ + Calculates average block time in milliseconds (based on the latest 100 blocks) divided by 2. + Sends corresponding requests to the RPC node. + Returns a tuple {:ok, block_check_interval, last_safe_block} + where `last_safe_block` is the number of the recent `safe` or `latest` block (depending on which one is available). + Returns {:error, description} in case of error. + """ + @spec get_block_check_interval(list()) :: {:ok, non_neg_integer(), non_neg_integer()} | {:error, any()} + def get_block_check_interval(json_rpc_named_arguments) do + {last_safe_block, _} = get_safe_block(json_rpc_named_arguments) + + first_block = max(last_safe_block - @block_check_interval_range_size, 1) + + with {:ok, first_block_timestamp} <- + get_block_timestamp_by_number(first_block, json_rpc_named_arguments, Helper.infinite_retries_number()), + {:ok, last_safe_block_timestamp} <- + get_block_timestamp_by_number(last_safe_block, json_rpc_named_arguments, Helper.infinite_retries_number()) do + block_check_interval = + ceil((last_safe_block_timestamp - first_block_timestamp) / (last_safe_block - first_block) * 1000 / 2) + + Logger.info("Block check interval is calculated as #{block_check_interval} ms.") + {:ok, block_check_interval, last_safe_block} + else + {:error, error} -> + {:error, "Failed to calculate block check interval due to #{inspect(error)}"} + end + end + + @doc """ + Fetches block number by its tag (e.g. `latest` or `safe`) using RPC request. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_block_number_by_tag(binary(), list(), non_neg_integer()) :: {:ok, non_neg_integer()} | {:error, atom()} + def get_block_number_by_tag(tag, json_rpc_named_arguments, retries \\ @finite_retries_number) do + error_message = &"Cannot fetch #{tag} block number. Error: #{inspect(&1)}" + + Helper.repeated_call( + &fetch_block_number_by_tag_op_version/2, + [tag, json_rpc_named_arguments], + error_message, + retries + ) + end + + @doc """ + Tries to get `safe` block number from the RPC node. + If it's not available, gets the `latest` one. + Returns a tuple of `{block_number, is_latest}` + where `is_latest` is true if the `safe` is not available. + """ + @spec get_safe_block(list()) :: {non_neg_integer(), boolean()} + def get_safe_block(json_rpc_named_arguments) do + case get_block_number_by_tag("safe", json_rpc_named_arguments) do + {:ok, safe_block} -> + {safe_block, false} + + {:error, :not_found} -> + {:ok, latest_block} = + get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) + + {latest_block, true} + end + end + + defp get_block_timestamp_by_number_inner(number, json_rpc_named_arguments) do + result = + %{id: 0, number: number} + |> ByNumber.request(false) + |> json_rpc(json_rpc_named_arguments) + + with {:ok, block} <- result, + false <- is_nil(block), + timestamp <- Map.get(block, "timestamp"), + false <- is_nil(timestamp) do + {:ok, quantity_to_integer(timestamp)} + else + {:error, message} -> + {:error, message} + + true -> + {:error, "RPC returned nil."} + end + end + + @doc """ + Fetches block timestamp by its number using RPC request. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_block_timestamp_by_number(non_neg_integer(), list(), non_neg_integer()) :: + {:ok, non_neg_integer()} | {:error, any()} + def get_block_timestamp_by_number(number, json_rpc_named_arguments, retries \\ @finite_retries_number) do + func = &get_block_timestamp_by_number_inner/2 + args = [number, json_rpc_named_arguments] + error_message = &"Cannot fetch block ##{number} or its timestamp. Error: #{inspect(&1)}" + Helper.repeated_call(func, args, error_message, retries) + end + + @doc """ + Fetches logs emitted by the specified contract (address) + within the specified block range and the first topic from the RPC node. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_logs( + non_neg_integer() | binary(), + non_neg_integer() | binary(), + binary(), + binary() | list(), + list(), + non_neg_integer() + ) :: {:ok, list()} | {:error, term()} + def get_logs(from_block, to_block, address, topic0, json_rpc_named_arguments, retries) do + processed_from_block = if is_integer(from_block), do: integer_to_quantity(from_block), else: from_block + processed_to_block = if is_integer(to_block), do: integer_to_quantity(to_block), else: to_block + + req = + request(%{ + id: 0, + method: "eth_getLogs", + params: [ + %{ + :fromBlock => processed_from_block, + :toBlock => processed_to_block, + :address => address, + :topics => [topic0] + } + ] + }) + + error_message = &"Cannot fetch logs for the block range #{from_block}..#{to_block}. Error: #{inspect(&1)}" + + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + end + + @doc """ + Fetches transaction data by its hash using RPC request. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_transaction_by_hash(binary() | nil, list(), non_neg_integer()) :: {:ok, any()} | {:error, any()} + def get_transaction_by_hash(hash, json_rpc_named_arguments, retries_left \\ @finite_retries_number) + + def get_transaction_by_hash(hash, _json_rpc_named_arguments, _retries_left) when is_nil(hash), do: {:ok, nil} + + def get_transaction_by_hash(hash, json_rpc_named_arguments, retries) do + req = + request(%{ + id: 0, + method: "eth_getTransactionByHash", + params: [hash] + }) + + error_message = &"eth_getTransactionByHash failed. Error: #{inspect(&1)}" + + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + end + + def get_logs_range_size do + @eth_get_logs_range_size + end + + @doc """ + Forms JSON RPC named arguments for the given RPC URL. + """ + @spec json_rpc_named_arguments(binary()) :: list() + def json_rpc_named_arguments(optimism_l1_rpc) do + [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: optimism_l1_rpc, + http_options: [ + recv_timeout: :timer.minutes(10), + timeout: :timer.minutes(10), + hackney: [pool: :ethereum_jsonrpc] + ] + ] + ] + end + + def init_continue(env, contract_address, caller) + when caller in [Indexer.Fetcher.Optimism.WithdrawalEvent, Indexer.Fetcher.Optimism.OutputRoot] do + {contract_name, table_name, start_block_note} = + if caller == Indexer.Fetcher.Optimism.WithdrawalEvent do + {"Optimism Portal", "op_withdrawal_events", "Withdrawals L1"} + else + {"Output Oracle", "op_output_roots", "Output Roots"} + end + + with {:start_block_l1_undefined, false} <- {:start_block_l1_undefined, is_nil(env[:start_block_l1])}, + {:reorg_monitor_started, true} <- + {:reorg_monitor_started, !is_nil(Process.whereis(Indexer.Fetcher.RollupL1ReorgMonitor))}, + optimism_l1_rpc = Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism][:optimism_l1_rpc], + {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(optimism_l1_rpc)}, + {:contract_is_valid, true} <- {:contract_is_valid, Helper.address_correct?(contract_address)}, + start_block_l1 = parse_integer(env[:start_block_l1]), + false <- is_nil(start_block_l1), + true <- start_block_l1 > 0, + {last_l1_block_number, last_l1_transaction_hash} <- caller.get_last_l1_item(), + {:start_block_l1_valid, true} <- + {:start_block_l1_valid, start_block_l1 <= last_l1_block_number || last_l1_block_number == 0}, + json_rpc_named_arguments = json_rpc_named_arguments(optimism_l1_rpc), + {:ok, last_l1_tx} <- get_transaction_by_hash(last_l1_transaction_hash, json_rpc_named_arguments), + {:l1_tx_not_found, false} <- {:l1_tx_not_found, !is_nil(last_l1_transaction_hash) && is_nil(last_l1_tx)}, + {:ok, block_check_interval, last_safe_block} <- get_block_check_interval(json_rpc_named_arguments) do + start_block = max(start_block_l1, last_l1_block_number) + + Process.send(self(), :continue, []) + + {:noreply, + %{ + contract_address: contract_address, + block_check_interval: block_check_interval, + start_block: start_block, + end_block: last_safe_block, + json_rpc_named_arguments: json_rpc_named_arguments + }} + else + {:start_block_l1_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, %{}} + + {:reorg_monitor_started, false} -> + Logger.error("Cannot start this process as reorg monitor in Indexer.Fetcher.Optimism is not started.") + {:stop, :normal, %{}} + + {:rpc_l1_undefined, true} -> + Logger.error("L1 RPC URL is not defined.") + {:stop, :normal, %{}} + + {:contract_is_valid, false} -> + Logger.error("#{contract_name} contract address is invalid or not defined.") + {:stop, :normal, %{}} + + {:start_block_l1_valid, false} -> + Logger.error("Invalid L1 Start Block value. Please, check the value and #{table_name} table.") + {:stop, :normal, %{}} + + {:error, error_data} -> + Logger.error( + "Cannot get last L1 transaction from RPC by its hash, last safe/latest block, or block timestamp by its number due to RPC error: #{inspect(error_data)}" + ) + + {:stop, :normal, %{}} + + {:l1_tx_not_found, true} -> + Logger.error( + "Cannot find last L1 transaction from RPC by its hash. Probably, there was a reorg on L1 chain. Please, check #{table_name} table." + ) + + {:stop, :normal, %{}} + + _ -> + Logger.error("#{start_block_note} Start Block is invalid or zero.") + {:stop, :normal, %{}} + end + end + + def repeated_request(req, error_message, json_rpc_named_arguments, retries) do + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex b/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex new file mode 100644 index 000000000000..c3a436358757 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex @@ -0,0 +1,565 @@ +defmodule Indexer.Fetcher.Optimism.Deposit do + @moduledoc """ + Fills op_deposits DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1, request: 1] + import Explorer.Helper, only: [decode_data: 2, parse_integer: 1] + + alias EthereumJSONRPC.Block.ByNumber + alias EthereumJSONRPC.Blocks + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Events.Publisher + alias Explorer.Chain.Optimism.Deposit + alias Indexer.Fetcher.Optimism + alias Indexer.Helper + + defstruct [ + :batch_size, + :start_block, + :from_block, + :safe_block, + :optimism_portal, + :json_rpc_named_arguments, + mode: :catch_up, + filter_id: nil, + check_interval: nil + ] + + # 32-byte signature of the event TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData) + @transaction_deposited_event "0xb3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32" + @retry_interval_minutes 3 + @retry_interval :timer.minutes(@retry_interval_minutes) + @address_prefix "0x000000000000000000000000" + @batch_size 500 + @fetcher_name :optimism_deposits + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, :ok}} + end + + @impl GenServer + def handle_continue(:ok, state) do + Logger.metadata(fetcher: @fetcher_name) + + env = Application.get_all_env(:indexer)[__MODULE__] + optimism_env = Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism] + optimism_portal = optimism_env[:optimism_l1_portal] + optimism_l1_rpc = optimism_env[:optimism_l1_rpc] + + with {:start_block_l1_undefined, false} <- {:start_block_l1_undefined, is_nil(env[:start_block_l1])}, + {:optimism_portal_valid, true} <- {:optimism_portal_valid, Helper.address_correct?(optimism_portal)}, + {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(optimism_l1_rpc)}, + start_block_l1 <- parse_integer(env[:start_block_l1]), + false <- is_nil(start_block_l1), + true <- start_block_l1 > 0, + {last_l1_block_number, last_l1_tx_hash} <- get_last_l1_item(), + json_rpc_named_arguments = Optimism.json_rpc_named_arguments(optimism_l1_rpc), + {:ok, last_l1_tx} <- Optimism.get_transaction_by_hash(last_l1_tx_hash, json_rpc_named_arguments), + {:l1_tx_not_found, false} <- {:l1_tx_not_found, !is_nil(last_l1_tx_hash) && is_nil(last_l1_tx)}, + {safe_block, _} = Optimism.get_safe_block(json_rpc_named_arguments), + {:start_block_l1_valid, true} <- + {:start_block_l1_valid, + (start_block_l1 <= last_l1_block_number || last_l1_block_number == 0) && start_block_l1 <= safe_block} do + start_block = max(start_block_l1, last_l1_block_number) + + if start_block > safe_block do + Process.send(self(), :switch_to_realtime, []) + else + Process.send(self(), :fetch, []) + end + + {:noreply, + %__MODULE__{ + start_block: start_block, + from_block: start_block, + safe_block: safe_block, + optimism_portal: optimism_portal, + json_rpc_named_arguments: json_rpc_named_arguments, + batch_size: parse_integer(env[:batch_size]) || @batch_size + }} + else + {:start_block_l1_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, state} + + {:start_block_l1_valid, false} -> + Logger.error("Invalid L1 Start Block value. Please, check the value and op_deposits table.") + {:stop, :normal, state} + + {:rpc_l1_undefined, true} -> + Logger.error("L1 RPC URL is not defined.") + {:stop, :normal, state} + + {:optimism_portal_valid, false} -> + Logger.error("OptimismPortal contract address is invalid or undefined.") + {:stop, :normal, state} + + {:error, error_data} -> + Logger.error("Cannot get last L1 transaction from RPC by its hash due to the RPC error: #{inspect(error_data)}") + + {:stop, :normal, state} + + {:l1_tx_not_found, true} -> + Logger.error( + "Cannot find last L1 transaction from RPC by its hash. Probably, there was a reorg on L1 chain. Please, check op_deposits table." + ) + + {:stop, :normal, state} + + _ -> + Logger.error("Optimism deposits L1 Start Block is invalid or zero.") + {:stop, :normal, state} + end + end + + @impl GenServer + def handle_info( + :fetch, + %__MODULE__{ + start_block: start_block, + from_block: from_block, + safe_block: safe_block, + optimism_portal: optimism_portal, + json_rpc_named_arguments: json_rpc_named_arguments, + mode: :catch_up, + batch_size: batch_size + } = state + ) do + to_block = min(from_block + batch_size, safe_block) + + with {:logs, {:ok, logs}} <- + {:logs, + Optimism.get_logs( + from_block, + to_block, + optimism_portal, + @transaction_deposited_event, + json_rpc_named_arguments, + 3 + )}, + _ = Helper.log_blocks_chunk_handling(from_block, to_block, start_block, safe_block, nil, :L1), + deposits = events_to_deposits(logs, json_rpc_named_arguments), + {:import, {:ok, _imported}} <- + {:import, Chain.import(%{optimism_deposits: %{params: deposits}, timeout: :infinity})} do + Publisher.broadcast(%{optimism_deposits: deposits}, :realtime) + + Helper.log_blocks_chunk_handling( + from_block, + to_block, + start_block, + safe_block, + "#{Enum.count(deposits)} TransactionDeposited event(s)", + :L1 + ) + + if to_block == safe_block do + Logger.info("Fetched all L1 blocks (#{start_block}..#{safe_block}), switching to realtime mode.") + Process.send(self(), :switch_to_realtime, []) + {:noreply, state} + else + Process.send(self(), :fetch, []) + {:noreply, %{state | from_block: to_block + 1}} + end + else + {:logs, {:error, _error}} -> + Logger.error("Cannot fetch logs. Retrying in #{@retry_interval_minutes} minutes...") + Process.send_after(self(), :fetch, @retry_interval) + {:noreply, state} + + {:import, {:error, error}} -> + Logger.error("Cannot import logs due to #{inspect(error)}. Retrying in #{@retry_interval_minutes} minutes...") + Process.send_after(self(), :fetch, @retry_interval) + {:noreply, state} + + {:import, {:error, step, failed_value, _changes_so_far}} -> + Logger.error( + "Failed to import #{inspect(failed_value)} during #{step}. Retrying in #{@retry_interval_minutes} minutes..." + ) + + Process.send_after(self(), :fetch, @retry_interval) + {:noreply, state} + end + end + + @impl GenServer + def handle_info( + :switch_to_realtime, + %__MODULE__{ + from_block: from_block, + safe_block: safe_block, + optimism_portal: optimism_portal, + json_rpc_named_arguments: json_rpc_named_arguments, + batch_size: batch_size, + mode: :catch_up + } = state + ) do + with {:check_interval, {:ok, check_interval, new_safe}} <- + {:check_interval, Optimism.get_block_check_interval(json_rpc_named_arguments)}, + {:catch_up, _, false} <- {:catch_up, new_safe, new_safe - safe_block + 1 > batch_size}, + {:logs, {:ok, logs}} <- + {:logs, + Optimism.get_logs( + max(safe_block, from_block), + "latest", + optimism_portal, + @transaction_deposited_event, + json_rpc_named_arguments, + 3 + )}, + {:ok, filter_id} <- + get_new_filter( + max(safe_block, from_block), + "latest", + optimism_portal, + @transaction_deposited_event, + json_rpc_named_arguments + ) do + handle_new_logs(logs, json_rpc_named_arguments) + Process.send(self(), :fetch, []) + {:noreply, %{state | mode: :realtime, filter_id: filter_id, check_interval: check_interval}} + else + {:catch_up, new_safe, true} -> + Process.send(self(), :fetch, []) + {:noreply, %{state | safe_block: new_safe}} + + {:logs, {:error, error}} -> + Logger.error("Failed to get logs while switching to realtime mode, reason: #{inspect(error)}") + Process.send_after(self(), :switch_to_realtime, @retry_interval) + {:noreply, state} + + {:error, _error} -> + Logger.error("Failed to set logs filter. Retrying in #{@retry_interval_minutes} minutes...") + Process.send_after(self(), :switch_to_realtime, @retry_interval) + {:noreply, state} + + {:check_interval, {:error, _error}} -> + Logger.error("Failed to calculate check_interval. Retrying in #{@retry_interval_minutes} minutes...") + Process.send_after(self(), :switch_to_realtime, @retry_interval) + {:noreply, state} + end + end + + @impl GenServer + def handle_info( + :fetch, + %__MODULE__{ + json_rpc_named_arguments: json_rpc_named_arguments, + mode: :realtime, + filter_id: filter_id, + check_interval: check_interval + } = state + ) do + case get_filter_changes(filter_id, json_rpc_named_arguments) do + {:ok, logs} -> + handle_new_logs(logs, json_rpc_named_arguments) + Process.send_after(self(), :fetch, check_interval) + {:noreply, state} + + {:error, :filter_not_found} -> + Logger.error("The old filter not found on the node. Creating new filter...") + Process.send(self(), :update_filter, []) + {:noreply, state} + + {:error, _error} -> + Logger.error("Failed to set logs filter. Retrying in #{@retry_interval_minutes} minutes...") + Process.send_after(self(), :fetch, @retry_interval) + {:noreply, state} + end + end + + @impl GenServer + def handle_info( + :update_filter, + %__MODULE__{ + optimism_portal: optimism_portal, + json_rpc_named_arguments: json_rpc_named_arguments, + mode: :realtime + } = state + ) do + {last_l1_block_number, _} = get_last_l1_item() + + case get_new_filter( + last_l1_block_number + 1, + "latest", + optimism_portal, + @transaction_deposited_event, + json_rpc_named_arguments + ) do + {:ok, filter_id} -> + Process.send(self(), :fetch, []) + {:noreply, %{state | filter_id: filter_id}} + + {:error, _error} -> + Logger.error("Failed to set logs filter. Retrying in #{@retry_interval_minutes} minutes...") + Process.send_after(self(), :update_filter, @retry_interval) + {:noreply, state} + end + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + @impl GenServer + def terminate( + _reason, + %__MODULE__{ + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + if state.filter_id do + Logger.info("Optimism deposits fetcher is terminating, uninstalling filter") + uninstall_filter(state.filter_id, json_rpc_named_arguments) + end + end + + @impl GenServer + def terminate(:normal, _state) do + :ok + end + + defp handle_new_logs(logs, json_rpc_named_arguments) do + {reorgs, logs_to_parse, min_block, max_block, cnt} = + logs + |> Enum.reduce({MapSet.new(), [], nil, 0, 0}, fn + %{"removed" => true, "blockNumber" => block_number}, {reorgs, logs_to_parse, min_block, max_block, cnt} -> + {MapSet.put(reorgs, block_number), logs_to_parse, min_block, max_block, cnt} + + %{"blockNumber" => block_number} = log, {reorgs, logs_to_parse, min_block, max_block, cnt} -> + { + reorgs, + [log | logs_to_parse], + min(min_block, quantity_to_integer(block_number)), + max(max_block, quantity_to_integer(block_number)), + cnt + 1 + } + end) + + handle_reorgs(reorgs) + + unless Enum.empty?(logs_to_parse) do + deposits = events_to_deposits(logs_to_parse, json_rpc_named_arguments) + {:ok, _imported} = Chain.import(%{optimism_deposits: %{params: deposits}, timeout: :infinity}) + + Publisher.broadcast(%{optimism_deposits: deposits}, :realtime) + + Helper.log_blocks_chunk_handling( + min_block, + max_block, + min_block, + max_block, + "#{cnt} TransactionDeposited event(s)", + :L1 + ) + end + end + + defp events_to_deposits(logs, json_rpc_named_arguments) do + timestamps = + logs + |> Enum.reduce(MapSet.new(), fn %{"blockNumber" => block_number_quantity}, acc -> + block_number = quantity_to_integer(block_number_quantity) + MapSet.put(acc, block_number) + end) + |> MapSet.to_list() + |> get_block_timestamps_by_numbers(json_rpc_named_arguments) + |> case do + {:ok, timestamps} -> + timestamps + + {:error, error} -> + Logger.error( + "Failed to get L1 block timestamps for deposits due to #{inspect(error)}. Timestamps will be set to null." + ) + + %{} + end + + Enum.map(logs, &event_to_deposit(&1, timestamps)) + end + + defp event_to_deposit( + %{ + "blockHash" => "0x" <> stripped_block_hash, + "blockNumber" => block_number_quantity, + "transactionHash" => transaction_hash, + "logIndex" => "0x" <> stripped_log_index, + "topics" => [_, @address_prefix <> from_stripped, @address_prefix <> to_stripped, _], + "data" => opaque_data + }, + timestamps + ) do + {_, prefixed_block_hash} = (String.pad_leading("", 64, "0") <> stripped_block_hash) |> String.split_at(-64) + {_, prefixed_log_index} = (String.pad_leading("", 64, "0") <> stripped_log_index) |> String.split_at(-64) + + deposit_id_hash = + "#{prefixed_block_hash}#{prefixed_log_index}" + |> Base.decode16!(case: :mixed) + |> ExKeccak.hash_256() + |> Base.encode16(case: :lower) + + source_hash = + "#{String.pad_leading("", 64, "0")}#{deposit_id_hash}" + |> Base.decode16!(case: :mixed) + |> ExKeccak.hash_256() + + [ + << + msg_value::binary-size(32), + value::binary-size(32), + gas_limit::binary-size(8), + is_creation::binary-size(1), + data::binary + >> + ] = decode_data(opaque_data, [:bytes]) + + rlp_encoded = + ExRLP.encode( + [ + source_hash, + from_stripped |> Base.decode16!(case: :mixed), + to_stripped |> Base.decode16!(case: :mixed), + msg_value |> String.replace_leading(<<0>>, <<>>), + value |> String.replace_leading(<<0>>, <<>>), + gas_limit |> String.replace_leading(<<0>>, <<>>), + is_creation |> String.replace_leading(<<0>>, <<>>), + data + ], + encoding: :hex + ) + + l2_tx_hash = + "0x" <> ("7e#{rlp_encoded}" |> Base.decode16!(case: :mixed) |> ExKeccak.hash_256() |> Base.encode16(case: :lower)) + + block_number = quantity_to_integer(block_number_quantity) + + %{ + l1_block_number: block_number, + l1_block_timestamp: Map.get(timestamps, block_number), + l1_transaction_hash: transaction_hash, + l1_transaction_origin: "0x" <> from_stripped, + l2_transaction_hash: l2_tx_hash + } + end + + defp handle_reorgs(reorgs) do + if MapSet.size(reorgs) > 0 do + Logger.warning("L1 reorg detected. The following L1 blocks were removed: #{inspect(MapSet.to_list(reorgs))}") + + {deleted_count, _} = Repo.delete_all(from(d in Deposit, where: d.l1_block_number in ^reorgs)) + + if deleted_count > 0 do + Logger.warning( + "As L1 reorg was detected, all affected rows were removed from the op_deposits table. Number of removed rows: #{deleted_count}." + ) + end + end + end + + defp get_block_timestamps_by_numbers(numbers, json_rpc_named_arguments, retries \\ 3) do + id_to_params = + numbers + |> Stream.map(fn number -> %{number: number} end) + |> Stream.with_index() + |> Enum.into(%{}, fn {params, id} -> {id, params} end) + + request = Blocks.requests(id_to_params, &ByNumber.request(&1, false)) + error_message = &"Cannot fetch timestamps for blocks #{numbers}. Error: #{inspect(&1)}" + + case Optimism.repeated_request(request, error_message, json_rpc_named_arguments, retries) do + {:ok, response} -> + %Blocks{blocks_params: blocks_params} = Blocks.from_responses(response, id_to_params) + + {:ok, + blocks_params + |> Enum.reduce(%{}, fn %{number: number, timestamp: timestamp}, acc -> Map.put_new(acc, number, timestamp) end)} + + err -> + err + end + end + + defp get_new_filter(from_block, to_block, address, topic0, json_rpc_named_arguments, retries \\ 3) do + processed_from_block = if is_integer(from_block), do: integer_to_quantity(from_block), else: from_block + processed_to_block = if is_integer(to_block), do: integer_to_quantity(to_block), else: to_block + + req = + request(%{ + id: 0, + method: "eth_newFilter", + params: [ + %{ + fromBlock: processed_from_block, + toBlock: processed_to_block, + address: address, + topics: [topic0] + } + ] + }) + + error_message = &"Cannot create new log filter. Error: #{inspect(&1)}" + + Optimism.repeated_request(req, error_message, json_rpc_named_arguments, retries) + end + + defp get_filter_changes(filter_id, json_rpc_named_arguments, retries \\ 3) do + req = + request(%{ + id: 0, + method: "eth_getFilterChanges", + params: [filter_id] + }) + + error_message = &"Cannot fetch filter changes. Error: #{inspect(&1)}" + + case Optimism.repeated_request(req, error_message, json_rpc_named_arguments, retries) do + {:error, %{code: _, message: "filter not found"}} -> {:error, :filter_not_found} + response -> response + end + end + + defp uninstall_filter(filter_id, json_rpc_named_arguments, retries \\ 1) do + req = + request(%{ + id: 0, + method: "eth_getFilterChanges", + params: [filter_id] + }) + + error_message = &"Cannot uninstall filter. Error: #{inspect(&1)}" + + Optimism.repeated_request(req, error_message, json_rpc_named_arguments, retries) + end + + defp get_last_l1_item do + Deposit.last_deposit_l1_block_number_query() + |> Repo.one() + |> Kernel.||({0, nil}) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/output_root.ex b/apps/indexer/lib/indexer/fetcher/optimism/output_root.ex new file mode 100644 index 000000000000..cbc21a8ddb2a --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism/output_root.ex @@ -0,0 +1,181 @@ +defmodule Indexer.Fetcher.Optimism.OutputRoot do + @moduledoc """ + Fills op_output_roots DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + alias Explorer.{Chain, Helper, Repo} + alias Explorer.Chain.Optimism.OutputRoot + alias Indexer.Fetcher.{Optimism, RollupL1ReorgMonitor} + alias Indexer.Helper, as: IndexerHelper + + @fetcher_name :optimism_output_roots + + # 32-byte signature of the event OutputProposed(bytes32 indexed outputRoot, uint256 indexed l2OutputIndex, uint256 indexed l2BlockNumber, uint256 l1Timestamp) + @output_proposed_event "0xa7aaf2512769da4e444e3de247be2564225c2e7a8f74cfe528e46e17d24868e2" + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, :ok}} + end + + @impl GenServer + def handle_continue(:ok, _state) do + Logger.metadata(fetcher: @fetcher_name) + + env = Application.get_all_env(:indexer)[__MODULE__] + + Optimism.init_continue(env, env[:output_oracle], __MODULE__) + end + + @impl GenServer + def handle_info( + :continue, + %{ + contract_address: output_oracle, + block_check_interval: block_check_interval, + start_block: start_block, + end_block: end_block, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + # credo:disable-for-next-line + time_before = Timex.now() + + chunks_number = ceil((end_block - start_block + 1) / Optimism.get_logs_range_size()) + chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) + + last_written_block = + chunk_range + |> Enum.reduce_while(start_block - 1, fn current_chunk, _ -> + chunk_start = start_block + Optimism.get_logs_range_size() * current_chunk + chunk_end = min(chunk_start + Optimism.get_logs_range_size() - 1, end_block) + + if chunk_end >= chunk_start do + IndexerHelper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) + + {:ok, result} = + Optimism.get_logs( + chunk_start, + chunk_end, + output_oracle, + @output_proposed_event, + json_rpc_named_arguments, + IndexerHelper.infinite_retries_number() + ) + + output_roots = events_to_output_roots(result) + + {:ok, _} = + Chain.import(%{ + optimism_output_roots: %{params: output_roots}, + timeout: :infinity + }) + + IndexerHelper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(output_roots)} OutputProposed event(s)", + :L1 + ) + end + + reorg_block = RollupL1ReorgMonitor.reorg_block_pop(__MODULE__) + + if !is_nil(reorg_block) && reorg_block > 0 do + {deleted_count, _} = Repo.delete_all(from(r in OutputRoot, where: r.l1_block_number >= ^reorg_block)) + + log_deleted_rows_count(reorg_block, deleted_count) + + {:halt, if(reorg_block <= chunk_end, do: reorg_block - 1, else: chunk_end)} + else + {:cont, chunk_end} + end + end) + + new_start_block = last_written_block + 1 + + {:ok, new_end_block} = + Optimism.get_block_number_by_tag("latest", json_rpc_named_arguments, IndexerHelper.infinite_retries_number()) + + delay = + if new_end_block == last_written_block do + # there is no new block, so wait for some time to let the chain issue the new block + max(block_check_interval - Timex.diff(Timex.now(), time_before, :milliseconds), 0) + else + 0 + end + + Process.send_after(self(), :continue, delay) + + {:noreply, %{state | start_block: new_start_block, end_block: new_end_block}} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + defp events_to_output_roots(events) do + Enum.map(events, fn event -> + [l1_timestamp] = Helper.decode_data(event["data"], [{:uint, 256}]) + {:ok, l1_timestamp} = DateTime.from_unix(l1_timestamp) + + %{ + l2_output_index: quantity_to_integer(Enum.at(event["topics"], 2)), + l2_block_number: quantity_to_integer(Enum.at(event["topics"], 3)), + l1_transaction_hash: event["transactionHash"], + l1_timestamp: l1_timestamp, + l1_block_number: quantity_to_integer(event["blockNumber"]), + output_root: Enum.at(event["topics"], 1) + } + end) + end + + defp log_deleted_rows_count(reorg_block, count) do + if count > 0 do + Logger.warning( + "As L1 reorg was detected, all rows with l1_block_number >= #{reorg_block} were removed from the op_output_roots table. Number of removed rows: #{count}." + ) + end + end + + def get_last_l1_item do + query = + from(root in OutputRoot, + select: {root.l1_block_number, root.l1_transaction_hash}, + order_by: [desc: root.l2_output_index], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/txn_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/txn_batch.ex new file mode 100644 index 000000000000..fa0dfcd371d4 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism/txn_batch.ex @@ -0,0 +1,1007 @@ +defmodule Indexer.Fetcher.Optimism.TxnBatch do + @moduledoc """ + Fills op_transaction_batches DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, only: [fetch_blocks_by_range: 2, json_rpc: 2, quantity_to_integer: 1] + + import Explorer.Helper, only: [parse_integer: 1] + + alias EthereumJSONRPC.Block.ByHash + alias EthereumJSONRPC.Blocks + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Beacon.Blob, as: BeaconBlob + alias Explorer.Chain.{Block, Hash} + alias Explorer.Chain.Optimism.FrameSequence + alias Explorer.Chain.Optimism.TxnBatch, as: OptimismTxnBatch + alias HTTPoison.Response + alias Indexer.Fetcher.Beacon.Blob + alias Indexer.Fetcher.Beacon.Client, as: BeaconClient + alias Indexer.Fetcher.{Optimism, RollupL1ReorgMonitor} + alias Indexer.Helper + alias Varint.LEB128 + + @fetcher_name :optimism_txn_batches + + # Optimism chain block time is a constant (2 seconds) + @op_chain_block_time 2 + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(args) do + {:ok, %{json_rpc_named_arguments_l2: args[:json_rpc_named_arguments]}, {:continue, nil}} + end + + @impl GenServer + def handle_continue(_, state) do + Logger.metadata(fetcher: @fetcher_name) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + Process.send_after(self(), :init_with_delay, 2000) + {:noreply, state} + end + + @impl GenServer + def handle_info(:init_with_delay, %{json_rpc_named_arguments_l2: json_rpc_named_arguments_l2} = state) do + env = Application.get_all_env(:indexer)[__MODULE__] + + with {:start_block_l1_undefined, false} <- {:start_block_l1_undefined, is_nil(env[:start_block_l1])}, + {:genesis_block_l2_invalid, false} <- + {:genesis_block_l2_invalid, is_nil(env[:genesis_block_l2]) or env[:genesis_block_l2] < 0}, + {:reorg_monitor_started, true} <- {:reorg_monitor_started, !is_nil(Process.whereis(RollupL1ReorgMonitor))}, + optimism_l1_rpc = Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism][:optimism_l1_rpc], + {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(optimism_l1_rpc)}, + {:blobs_api_url_undefined, false} <- {:blobs_api_url_undefined, is_nil(env[:blobs_api_url])}, + {:batch_inbox_valid, true} <- {:batch_inbox_valid, Helper.address_correct?(env[:batch_inbox])}, + {:batch_submitter_valid, true} <- {:batch_submitter_valid, Helper.address_correct?(env[:batch_submitter])}, + start_block_l1 = parse_integer(env[:start_block_l1]), + false <- is_nil(start_block_l1), + true <- start_block_l1 > 0, + chunk_size = parse_integer(env[:blocks_chunk_size]), + {:chunk_size_valid, true} <- {:chunk_size_valid, !is_nil(chunk_size) && chunk_size > 0}, + json_rpc_named_arguments = Optimism.json_rpc_named_arguments(optimism_l1_rpc), + {last_l1_block_number, last_l1_transaction_hash, last_l1_tx} = get_last_l1_item(json_rpc_named_arguments), + {:start_block_l1_valid, true} <- + {:start_block_l1_valid, start_block_l1 <= last_l1_block_number || last_l1_block_number == 0}, + {:l1_tx_not_found, false} <- {:l1_tx_not_found, !is_nil(last_l1_transaction_hash) && is_nil(last_l1_tx)}, + {:ok, block_check_interval, last_safe_block} <- Optimism.get_block_check_interval(json_rpc_named_arguments) do + start_block = max(start_block_l1, last_l1_block_number) + + Process.send(self(), :continue, []) + + {:noreply, + %{ + batch_inbox: String.downcase(env[:batch_inbox]), + batch_submitter: String.downcase(env[:batch_submitter]), + blobs_api_url: String.trim_trailing(env[:blobs_api_url], "/"), + block_check_interval: block_check_interval, + start_block: start_block, + end_block: last_safe_block, + chunk_size: chunk_size, + incomplete_channels: %{}, + genesis_block_l2: env[:genesis_block_l2], + json_rpc_named_arguments: json_rpc_named_arguments, + json_rpc_named_arguments_l2: json_rpc_named_arguments_l2 + }} + else + {:start_block_l1_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, state} + + {:genesis_block_l2_invalid, true} -> + Logger.error("L2 genesis block number is undefined or invalid.") + {:stop, :normal, state} + + {:reorg_monitor_started, false} -> + Logger.error( + "Cannot start this process as reorg monitor in Indexer.Fetcher.RollupL1ReorgMonitor is not started." + ) + + {:stop, :normal, state} + + {:rpc_l1_undefined, true} -> + Logger.error("L1 RPC URL is not defined.") + {:stop, :normal, state} + + {:blobs_api_url_undefined, true} -> + Logger.error("L1 Blockscout Blobs API URL is not defined.") + {:stop, :normal, state} + + {:batch_inbox_valid, false} -> + Logger.error("Batch Inbox address is invalid or not defined.") + {:stop, :normal, state} + + {:batch_submitter_valid, false} -> + Logger.error("Batch Submitter address is invalid or not defined.") + {:stop, :normal, state} + + {:start_block_l1_valid, false} -> + Logger.error("Invalid L1 Start Block value. Please, check the value and op_transaction_batches table.") + {:stop, :normal, state} + + {:chunk_size_valid, false} -> + Logger.error("Invalid blocks chunk size value.") + {:stop, :normal, state} + + {:error, error_data} -> + Logger.error("Cannot get block timestamp by its number due to RPC error: #{inspect(error_data)}") + + {:stop, :normal, state} + + {:l1_tx_not_found, true} -> + Logger.error( + "Cannot find last L1 transaction from RPC by its hash. Probably, there was a reorg on L1 chain. Please, check op_transaction_batches table." + ) + + {:stop, :normal, state} + + _ -> + Logger.error("Batch Start Block is invalid or zero.") + {:stop, :normal, state} + end + end + + @impl GenServer + def handle_info( + :continue, + %{ + batch_inbox: batch_inbox, + batch_submitter: batch_submitter, + blobs_api_url: blobs_api_url, + block_check_interval: block_check_interval, + start_block: start_block, + end_block: end_block, + chunk_size: chunk_size, + incomplete_channels: incomplete_channels, + genesis_block_l2: genesis_block_l2, + json_rpc_named_arguments: json_rpc_named_arguments, + json_rpc_named_arguments_l2: json_rpc_named_arguments_l2 + } = state + ) do + time_before = Timex.now() + + chunks_number = ceil((end_block - start_block + 1) / chunk_size) + chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) + + {last_written_block, new_incomplete_channels} = + chunk_range + |> Enum.reduce_while({start_block - 1, incomplete_channels}, fn current_chunk, {_, incomplete_channels_acc} -> + chunk_start = start_block + chunk_size * current_chunk + chunk_end = min(chunk_start + chunk_size - 1, end_block) + + new_incomplete_channels = + if chunk_end >= chunk_start do + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) + + {:ok, new_incomplete_channels, batches, sequences} = + get_txn_batches( + Range.new(chunk_start, chunk_end), + batch_inbox, + batch_submitter, + genesis_block_l2, + incomplete_channels_acc, + {json_rpc_named_arguments, json_rpc_named_arguments_l2}, + blobs_api_url, + Helper.infinite_retries_number() + ) + + {batches, sequences} = remove_duplicates(batches, sequences) + + {:ok, _} = + Chain.import(%{ + optimism_frame_sequences: %{params: sequences}, + optimism_txn_batches: %{params: batches}, + timeout: :infinity + }) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(sequences)} batch(es) containing #{Enum.count(batches)} block(s).", + :L1 + ) + + new_incomplete_channels + else + incomplete_channels_acc + end + + reorg_block = RollupL1ReorgMonitor.reorg_block_pop(__MODULE__) + + if !is_nil(reorg_block) && reorg_block > 0 do + new_incomplete_channels = handle_l1_reorg(reorg_block, new_incomplete_channels) + {:halt, {if(reorg_block <= chunk_end, do: reorg_block - 1, else: chunk_end), new_incomplete_channels}} + else + {:cont, {chunk_end, new_incomplete_channels}} + end + end) + + new_start_block = last_written_block + 1 + + {:ok, new_end_block} = + Optimism.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) + + delay = + if new_end_block == last_written_block do + # there is no new block, so wait for some time to let the chain issue the new block + max(block_check_interval - Timex.diff(Timex.now(), time_before, :milliseconds), 0) + else + 0 + end + + Process.send_after(self(), :continue, delay) + + {:noreply, + %{ + state + | start_block: new_start_block, + end_block: new_end_block, + incomplete_channels: new_incomplete_channels + }} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + defp get_block_numbers_by_hashes([], _json_rpc_named_arguments_l2) do + %{} + end + + defp get_block_numbers_by_hashes(hashes, json_rpc_named_arguments_l2) do + query = + from( + b in Block, + select: {b.hash, b.number}, + where: b.hash in ^hashes + ) + + number_by_hash = + query + |> Repo.all(timeout: :infinity) + |> Enum.reduce(%{}, fn {hash, number}, acc -> + Map.put(acc, hash.bytes, number) + end) + + requests = + hashes + |> Enum.filter(fn hash -> is_nil(Map.get(number_by_hash, hash)) end) + |> Enum.with_index() + |> Enum.map(fn {hash, id} -> + ByHash.request(%{hash: "0x" <> Base.encode16(hash, case: :lower), id: id}, false) + end) + + chunk_size = 50 + chunks_number = ceil(Enum.count(requests) / chunk_size) + chunk_range = Range.new(0, chunks_number - 1, 1) + + chunk_range + |> Enum.reduce([], fn current_chunk, acc -> + {:ok, resp} = + requests + |> Enum.slice(chunk_size * current_chunk, chunk_size) + |> json_rpc(json_rpc_named_arguments_l2) + + acc ++ resp + end) + |> Enum.map(fn %{result: result} -> result end) + |> Enum.reduce(number_by_hash, fn block, acc -> + if is_nil(block) do + acc + else + block_number = quantity_to_integer(Map.get(block, "number")) + "0x" <> hash = Map.get(block, "hash") + {:ok, hash} = Base.decode16(hash, case: :lower) + Map.put(acc, hash, block_number) + end + end) + end + + defp get_block_timestamp_by_number(block_number, blocks_params) do + block = Enum.find(blocks_params, %{timestamp: nil}, fn b -> b.number == block_number end) + block.timestamp + end + + defp get_last_l1_item(json_rpc_named_arguments) do + l1_transaction_hashes = + Repo.one( + from( + tb in OptimismTxnBatch, + inner_join: fs in FrameSequence, + on: fs.id == tb.frame_sequence_id, + select: fs.l1_transaction_hashes, + order_by: [desc: tb.l2_block_number], + limit: 1 + ) + ) + + last_l1_transaction_hash = + if is_nil(l1_transaction_hashes) do + nil + else + List.last(l1_transaction_hashes) + end + + if is_nil(last_l1_transaction_hash) do + {0, nil, nil} + else + {:ok, last_l1_tx} = Optimism.get_transaction_by_hash(last_l1_transaction_hash, json_rpc_named_arguments) + last_l1_block_number = quantity_to_integer(Map.get(last_l1_tx || %{}, "blockNumber", 0)) + {last_l1_block_number, last_l1_transaction_hash, last_l1_tx} + end + end + + defp get_txn_batches( + block_range, + batch_inbox, + batch_submitter, + genesis_block_l2, + incomplete_channels, + {json_rpc_named_arguments, json_rpc_named_arguments_l2}, + blobs_api_url, + retries_left + ) do + case fetch_blocks_by_range(block_range, json_rpc_named_arguments) do + {:ok, %Blocks{transactions_params: transactions_params, blocks_params: blocks_params, errors: []}} -> + transactions_params + |> txs_filter(batch_submitter, batch_inbox) + |> get_txn_batches_inner( + blocks_params, + genesis_block_l2, + incomplete_channels, + json_rpc_named_arguments_l2, + blobs_api_url + ) + + {_, message_or_errors} -> + message = + case message_or_errors do + %Blocks{errors: errors} -> errors + msg -> msg + end + + retries_left = retries_left - 1 + + error_message = "Cannot fetch blocks #{inspect(block_range)}. Error(s): #{inspect(message)}" + + if retries_left <= 0 do + Logger.error(error_message) + {:error, message} + else + Logger.error("#{error_message} Retrying...") + :timer.sleep(3000) + + get_txn_batches( + block_range, + batch_inbox, + batch_submitter, + genesis_block_l2, + incomplete_channels, + {json_rpc_named_arguments, json_rpc_named_arguments_l2}, + blobs_api_url, + retries_left + ) + end + end + end + + defp blobs_to_inputs(transaction_hash, blob_versioned_hashes, block_timestamp, blobs_api_url) do + blob_versioned_hashes + |> Enum.reduce([], fn blob_hash, acc -> + with {:ok, response} <- http_get_request(blobs_api_url <> "/" <> blob_hash), + blob_data = Map.get(response, "blob_data"), + false <- is_nil(blob_data) do + # read the data from Blockscout API + decoded = + blob_data + |> String.trim_leading("0x") + |> Base.decode16!(case: :lower) + |> OptimismTxnBatch.decode_eip4844_blob() + + if is_nil(decoded) do + Logger.warning("Cannot decode the blob #{blob_hash} taken from the Blockscout Blobs API.") + acc + else + Logger.info( + "The input for transaction #{transaction_hash} is taken from the Blockscout Blobs API. Blob hash: #{blob_hash}" + ) + + [decoded | acc] + end + else + _ -> + # read the data from the fallback source (beacon node) + + beacon_config = + :indexer + |> Application.get_env(Blob) + |> Keyword.take([:reference_slot, :reference_timestamp, :slot_duration]) + |> Enum.into(%{}) + + try do + {:ok, fetched_blobs} = + block_timestamp + |> DateTime.to_unix() + |> Blob.timestamp_to_slot(beacon_config) + |> BeaconClient.get_blob_sidecars() + + blobs = Map.get(fetched_blobs, "data", []) + + if Enum.empty?(blobs) do + raise "Empty data" + end + + decoded_blob_data = + blobs + |> Enum.find(fn b -> + b + |> Map.get("kzg_commitment", "0x") + |> String.trim_leading("0x") + |> Base.decode16!(case: :lower) + |> BeaconBlob.hash() + |> Hash.to_string() + |> Kernel.==(blob_hash) + end) + |> Map.get("blob") + |> String.trim_leading("0x") + |> Base.decode16!(case: :lower) + |> OptimismTxnBatch.decode_eip4844_blob() + + if is_nil(decoded_blob_data) do + raise "Invalid blob" + else + Logger.info( + "The input for transaction #{transaction_hash} is taken from the Beacon Node. Blob hash: #{blob_hash}" + ) + + [decoded_blob_data | acc] + end + rescue + reason -> + Logger.warning( + "Cannot decode the blob #{blob_hash} taken from the Beacon Node. Reason: #{inspect(reason)}" + ) + + acc + end + end + end) + |> Enum.reverse() + end + + defp get_txn_batches_inner( + transactions_filtered, + blocks_params, + genesis_block_l2, + incomplete_channels, + json_rpc_named_arguments_l2, + blobs_api_url + ) do + transactions_filtered + |> Enum.reduce({:ok, incomplete_channels, [], []}, fn tx, + {_, incomplete_channels_acc, batches_acc, sequences_acc} -> + inputs = + if tx.type == 3 do + # this is EIP-4844 transaction, so we get the inputs from the blobs + block_timestamp = get_block_timestamp_by_number(tx.block_number, blocks_params) + blobs_to_inputs(tx.hash, tx.blob_versioned_hashes, block_timestamp, blobs_api_url) + else + [tx.input] + end + + Enum.reduce(inputs, {:ok, incomplete_channels_acc, batches_acc, sequences_acc}, fn input, + {_, + new_incomplete_channels_acc, + new_batches_acc, + new_sequences_acc} -> + handle_input( + input, + tx, + blocks_params, + new_incomplete_channels_acc, + new_batches_acc, + new_sequences_acc, + genesis_block_l2, + json_rpc_named_arguments_l2 + ) + end) + end) + end + + defp handle_input( + input, + tx, + blocks_params, + incomplete_channels_acc, + batches_acc, + sequences_acc, + genesis_block_l2, + json_rpc_named_arguments_l2 + ) do + frame = input_to_frame(input) + + channel = Map.get(incomplete_channels_acc, frame.channel_id, %{frames: %{}}) + + channel_frames = + Map.put(channel.frames, frame.number, %{ + data: frame.data, + is_last: frame.is_last, + block_number: tx.block_number, + tx_hash: tx.hash + }) + + l1_timestamp = + if frame.is_last do + get_block_timestamp_by_number(tx.block_number, blocks_params) + else + Map.get(channel, :l1_timestamp) + end + + channel_updated = + channel + |> Map.put_new(:id, frame.channel_id) + |> Map.put(:frames, channel_frames) + |> Map.put(:timestamp, DateTime.utc_now()) + |> Map.put(:l1_timestamp, l1_timestamp) + + if channel_complete?(channel_updated) do + handle_channel( + channel_updated, + incomplete_channels_acc, + batches_acc, + sequences_acc, + genesis_block_l2, + json_rpc_named_arguments_l2 + ) + else + {:ok, Map.put(incomplete_channels_acc, frame.channel_id, channel_updated), batches_acc, sequences_acc} + end + end + + defp handle_channel( + channel, + incomplete_channels_acc, + batches_acc, + sequences_acc, + genesis_block_l2, + json_rpc_named_arguments_l2 + ) do + frame_sequence_last = List.first(sequences_acc) + frame_sequence_id = next_frame_sequence_id(frame_sequence_last) + + {bytes, l1_transaction_hashes} = + 0..(Enum.count(channel.frames) - 1) + |> Enum.reduce({<<>>, []}, fn frame_number, {bytes_acc, tx_hashes_acc} -> + frame = Map.get(channel.frames, frame_number) + {bytes_acc <> frame.data, [frame.tx_hash | tx_hashes_acc]} + end) + + batches_parsed = + parse_frame_sequence( + bytes, + frame_sequence_id, + channel.l1_timestamp, + genesis_block_l2, + json_rpc_named_arguments_l2 + ) + + if batches_parsed == :error do + Logger.error("Cannot parse frame sequence from these L1 transaction(s): #{inspect(l1_transaction_hashes)}") + end + + seq = %{ + id: frame_sequence_id, + l1_transaction_hashes: Enum.reverse(l1_transaction_hashes), + l1_timestamp: channel.l1_timestamp + } + + new_incomplete_channels_acc = + incomplete_channels_acc + |> Map.delete(channel.id) + |> remove_expired_channels() + + if batches_parsed == :error or Enum.empty?(batches_parsed) do + {:ok, new_incomplete_channels_acc, batches_acc, sequences_acc} + else + {:ok, new_incomplete_channels_acc, batches_acc ++ batches_parsed, [seq | sequences_acc]} + end + end + + defp handle_l1_reorg(reorg_block, incomplete_channels) do + incomplete_channels + |> Enum.reduce(incomplete_channels, fn {channel_id, %{frames: frames} = channel}, acc -> + updated_frames = + frames + |> Enum.filter(fn {_frame_number, %{block_number: block_number}} -> + block_number < reorg_block + end) + |> Enum.into(%{}) + + if Enum.empty?(updated_frames) do + Map.delete(acc, channel_id) + else + Map.put(acc, channel_id, Map.put(channel, :frames, updated_frames)) + end + end) + end + + @doc """ + Removes rows from op_transaction_batches and op_frame_sequences tables written beginning from the L2 reorg block. + """ + @spec handle_l2_reorg(non_neg_integer()) :: any() + def handle_l2_reorg(reorg_block) do + frame_sequence_ids = + Repo.all( + from( + tb in OptimismTxnBatch, + select: tb.frame_sequence_id, + where: tb.l2_block_number >= ^reorg_block + ), + timeout: :infinity + ) + + {deleted_count, _} = Repo.delete_all(from(tb in OptimismTxnBatch, where: tb.l2_block_number >= ^reorg_block)) + + Repo.delete_all(from(fs in FrameSequence, where: fs.id in ^frame_sequence_ids)) + + if deleted_count > 0 do + Logger.warning( + "As L2 reorg was detected, all rows with l2_block_number >= #{reorg_block} were removed from the op_transaction_batches table. Number of removed rows: #{deleted_count}." + ) + end + end + + defp http_get_request(url) do + case Application.get_env(:explorer, :http_adapter).get(url) do + {:ok, %Response{body: body, status_code: 200}} -> + Jason.decode(body) + + {:ok, %Response{body: body, status_code: _}} -> + {:error, body} + + {:error, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to Blockscout Blobs API: #{url}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, "Error while sending request to Blockscout Blobs API"} + end + end + + defp channel_complete?(channel) do + last_frame_number = + channel.frames + |> Map.keys() + |> Enum.max() + + Map.get(channel.frames, last_frame_number).is_last and last_frame_number == Enum.count(channel.frames) - 1 + end + + defp remove_expired_channels(channels_map) do + now = DateTime.utc_now() + + Enum.reduce(channels_map, channels_map, fn {channel_id, %{timestamp: timestamp}}, channels_acc -> + if DateTime.diff(now, timestamp) >= 86400 do + Map.delete(channels_acc, channel_id) + else + channels_acc + end + end) + end + + defp input_to_frame("0x" <> input) do + input + |> Base.decode16!(case: :mixed) + |> input_to_frame() + end + + defp input_to_frame(input_binary) do + # the structure of the input is as follows: + # + # input = derivation_version ++ channel_id ++ frame_number ++ frame_data_length ++ frame_data ++ is_last + # + # derivation_version = uint8 + # channel_id = bytes16 + # frame_number = uint16 + # frame_data_length = uint32 + # frame_data = bytes + # is_last = bool (uint8) + + derivation_version_length = 1 + channel_id_length = 16 + frame_number_size = 2 + frame_data_length_size = 4 + is_last_size = 1 + + # the first byte must be zero (so called Derivation Version) + [0] = :binary.bin_to_list(binary_part(input_binary, 0, derivation_version_length)) + + # channel id has 16 bytes + channel_id = binary_part(input_binary, derivation_version_length, channel_id_length) + + # frame number consists of 2 bytes + frame_number_offset = derivation_version_length + channel_id_length + frame_number = :binary.decode_unsigned(binary_part(input_binary, frame_number_offset, frame_number_size)) + + # frame data length consists of 4 bytes + frame_data_length_offset = frame_number_offset + frame_number_size + + frame_data_length = + :binary.decode_unsigned(binary_part(input_binary, frame_data_length_offset, frame_data_length_size)) + + input_length_must_be = + derivation_version_length + channel_id_length + frame_number_size + frame_data_length_size + frame_data_length + + is_last_size + + input_length_current = byte_size(input_binary) + + if input_length_current == input_length_must_be do + # frame data is a byte array of frame_data_length size + frame_data_offset = frame_data_length_offset + frame_data_length_size + frame_data = binary_part(input_binary, frame_data_offset, frame_data_length) + + # is_last is 1-byte item + is_last_offset = frame_data_offset + frame_data_length + is_last = :binary.decode_unsigned(binary_part(input_binary, is_last_offset, is_last_size)) > 0 + + %{number: frame_number, data: frame_data, is_last: is_last, channel_id: channel_id} + else + # workaround to remove a leading extra byte + # for example, the case for Base Goerli batch L1 transaction: https://goerli.etherscan.io/tx/0xa43fa9da683a6157a114e3175a625b5aed85d8c573aae226768c58a924a17be0 + input_to_frame("0x" <> Base.encode16(binary_part(input_binary, 1, input_length_current - 1))) + end + end + + defp next_frame_sequence_id(last_known_sequence) when is_nil(last_known_sequence) do + last_known_id = + Repo.one( + from( + fs in FrameSequence, + select: fs.id, + order_by: [desc: fs.id], + limit: 1 + ) + ) + + if is_nil(last_known_id) do + 1 + else + last_known_id + 1 + end + end + + defp next_frame_sequence_id(last_known_sequence) do + last_known_sequence.id + 1 + end + + defp parse_frame_sequence( + bytes, + id, + l1_timestamp, + genesis_block_l2, + json_rpc_named_arguments_l2 + ) do + uncompressed_bytes = zlib_decompress(bytes) + + batches = + Enum.reduce_while(Stream.iterate(0, &(&1 + 1)), {uncompressed_bytes, []}, fn _i, {remainder, batch_acc} -> + try do + {decoded, new_remainder} = ExRLP.decode(remainder, stream: true) + + <> = binary_part(decoded, 0, 1) + content = binary_part(decoded, 1, byte_size(decoded) - 1) + + new_batch_acc = + cond do + version == 0 -> + handle_v0_batch(content, id, l1_timestamp, batch_acc) + + version <= 2 -> + # parsing the span batch + handle_v1_batch(content, id, l1_timestamp, genesis_block_l2, batch_acc) + + true -> + Logger.error("Unsupported batch version ##{version}") + :error + end + + if byte_size(new_remainder) > 0 and new_batch_acc != :error do + {:cont, {new_remainder, new_batch_acc}} + else + {:halt, new_batch_acc} + end + rescue + _ -> {:halt, :error} + end + end) + + if batches == :error do + :error + else + batches = Enum.reverse(batches) + + numbers_by_hashes = + batches + |> Stream.filter(&Map.has_key?(&1, :parent_hash)) + |> Enum.map(fn batch -> batch.parent_hash end) + |> get_block_numbers_by_hashes(json_rpc_named_arguments_l2) + + Enum.map(batches, &parent_hash_to_l2_block_number(&1, numbers_by_hashes)) + end + end + + defp handle_v0_batch(content, frame_sequence_id, l1_timestamp, batch_acc) do + content_decoded = ExRLP.decode(content) + + batch = %{ + parent_hash: Enum.at(content_decoded, 0), + frame_sequence_id: frame_sequence_id, + l1_timestamp: l1_timestamp + } + + [batch | batch_acc] + end + + defp handle_v1_batch(content, frame_sequence_id, l1_timestamp, genesis_block_l2, batch_acc) do + {rel_timestamp, content_remainder} = LEB128.decode(content) + + # skip l1_origin_num + {_l1_origin_num, checks_and_payload} = LEB128.decode(content_remainder) + + # skip `parent_check` and `l1_origin_check` fields (20 bytes each) + # and read the block count + {block_count, _} = + checks_and_payload + |> binary_part(40, byte_size(checks_and_payload) - 40) + |> LEB128.decode() + + # the first and last L2 blocks in the span + span_start = div(rel_timestamp, @op_chain_block_time) + genesis_block_l2 + span_end = span_start + block_count - 1 + + cond do + rem(rel_timestamp, @op_chain_block_time) != 0 -> + Logger.error("rel_timestamp is not divisible by #{@op_chain_block_time}. We ignore the span batch.") + batch_acc + + block_count <= 0 -> + Logger.error("Empty span batch found. We ignore it.") + batch_acc + + true -> + span_start..span_end + |> Enum.reduce(batch_acc, fn l2_block_number, batch_acc -> + [ + %{ + l2_block_number: l2_block_number, + frame_sequence_id: frame_sequence_id, + l1_timestamp: l1_timestamp + } + | batch_acc + ] + end) + end + end + + defp parent_hash_to_l2_block_number(batch, numbers_by_hashes) do + if Map.has_key?(batch, :parent_hash) do + number = Map.get(numbers_by_hashes, batch.parent_hash) + + batch + |> Map.put(:l2_block_number, number + 1) + |> Map.delete(:parent_hash) + else + batch + end + end + + defp remove_duplicates(batches, sequences) do + unique_batches = + batches + |> Enum.sort(fn b1, b2 -> + b1.l2_block_number < b2.l2_block_number or + (b1.l2_block_number == b2.l2_block_number and b1.l1_timestamp < b2.l1_timestamp) + end) + |> Enum.reduce(%{}, fn b, acc -> + Map.put(acc, b.l2_block_number, Map.delete(b, :l1_timestamp)) + end) + |> Map.values() + + unique_sequences = + if Enum.empty?(sequences) do + [] + else + sequences + |> Enum.reverse() + |> Enum.filter(fn seq -> + Enum.any?(unique_batches, fn batch -> batch.frame_sequence_id == seq.id end) + end) + end + + {unique_batches, unique_sequences} + end + + defp txs_filter(transactions_params, batch_submitter, batch_inbox) do + transactions_params + |> Enum.filter(fn t -> + from_address_hash = Map.get(t, :from_address_hash) + to_address_hash = Map.get(t, :to_address_hash) + + if is_nil(from_address_hash) or is_nil(to_address_hash) do + false + else + String.downcase(from_address_hash) == batch_submitter and String.downcase(to_address_hash) == batch_inbox + end + end) + end + + defp zlib_decompress(bytes) do + z = :zlib.open() + :zlib.inflateInit(z) + + uncompressed_bytes = + try do + zlib_inflate(z, bytes) + rescue + _ -> <<>> + end + + try do + :zlib.inflateEnd(z) + rescue + _ -> nil + end + + :zlib.close(z) + + uncompressed_bytes + end + + defp zlib_inflate_handler(z, {:continue, [uncompressed_bytes]}, acc) do + zlib_inflate(z, [], acc <> uncompressed_bytes) + end + + defp zlib_inflate_handler(_z, {:finished, [uncompressed_bytes]}, acc) do + acc <> uncompressed_bytes + end + + defp zlib_inflate_handler(_z, {:finished, []}, acc) do + acc + end + + defp zlib_inflate(z, compressed_bytes, acc \\ <<>>) do + result = :zlib.safeInflate(z, compressed_bytes) + zlib_inflate_handler(z, result, acc) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex new file mode 100644 index 000000000000..aac9684bce25 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex @@ -0,0 +1,361 @@ +defmodule Indexer.Fetcher.Optimism.Withdrawal do + @moduledoc """ + Fills op_withdrawals DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, only: [quantity_to_integer: 1] + import Explorer.Helper, only: [decode_data: 2, parse_integer: 1] + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Log + alias Explorer.Chain.Optimism.Withdrawal, as: OptimismWithdrawal + alias Indexer.Fetcher.Optimism + alias Indexer.Helper + + @fetcher_name :optimism_withdrawals + + # 32-byte signature of the event MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash) + @message_passed_event "0x02a52367d10742d8032712c1bb8e0144ff1ec5ffda1ed7d70bb05a2744955054" + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(args) do + json_rpc_named_arguments = args[:json_rpc_named_arguments] + {:ok, %{}, {:continue, json_rpc_named_arguments}} + end + + @impl GenServer + def handle_continue(json_rpc_named_arguments, state) do + Logger.metadata(fetcher: @fetcher_name) + + env = Application.get_all_env(:indexer)[__MODULE__] + + with {:start_block_l2_undefined, false} <- {:start_block_l2_undefined, is_nil(env[:start_block_l2])}, + {:message_passer_valid, true} <- {:message_passer_valid, Helper.address_correct?(env[:message_passer])}, + start_block_l2 = parse_integer(env[:start_block_l2]), + false <- is_nil(start_block_l2), + true <- start_block_l2 > 0, + {last_l2_block_number, last_l2_transaction_hash} <- get_last_l2_item(), + {safe_block, safe_block_is_latest} = Optimism.get_safe_block(json_rpc_named_arguments), + {:start_block_l2_valid, true} <- + {:start_block_l2_valid, + (start_block_l2 <= last_l2_block_number || last_l2_block_number == 0) && start_block_l2 <= safe_block}, + {:ok, last_l2_tx} <- Optimism.get_transaction_by_hash(last_l2_transaction_hash, json_rpc_named_arguments), + {:l2_tx_not_found, false} <- {:l2_tx_not_found, !is_nil(last_l2_transaction_hash) && is_nil(last_l2_tx)} do + Process.send(self(), :continue, []) + + {:noreply, + %{ + start_block: max(start_block_l2, last_l2_block_number), + start_block_l2: start_block_l2, + safe_block: safe_block, + safe_block_is_latest: safe_block_is_latest, + message_passer: env[:message_passer], + json_rpc_named_arguments: json_rpc_named_arguments + }} + else + {:start_block_l2_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, state} + + {:message_passer_valid, false} -> + Logger.error("L2ToL1MessagePasser contract address is invalid or not defined.") + {:stop, :normal, state} + + {:start_block_l2_valid, false} -> + Logger.error("Invalid L2 Start Block value. Please, check the value and op_withdrawals table.") + {:stop, :normal, state} + + {:error, error_data} -> + Logger.error("Cannot get last L2 transaction from RPC by its hash due to RPC error: #{inspect(error_data)}") + + {:stop, :normal, state} + + {:l2_tx_not_found, true} -> + Logger.error( + "Cannot find last L2 transaction from RPC by its hash. Probably, there was a reorg on L2 chain. Please, check op_withdrawals table." + ) + + {:stop, :normal, state} + + _ -> + Logger.error("Withdrawals L2 Start Block is invalid or zero.") + {:stop, :normal, state} + end + end + + @impl GenServer + def handle_info( + :continue, + %{ + start_block_l2: start_block_l2, + message_passer: message_passer, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + fill_msg_nonce_gaps(start_block_l2, message_passer, json_rpc_named_arguments) + Process.send(self(), :find_new_events, []) + {:noreply, state} + end + + @impl GenServer + def handle_info( + :find_new_events, + %{ + start_block: start_block, + safe_block: safe_block, + safe_block_is_latest: safe_block_is_latest, + message_passer: message_passer, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + # find and fill all events between start_block and "safe" block + # the "safe" block can be "latest" (when safe_block_is_latest == true) + fill_block_range(start_block, safe_block, message_passer, json_rpc_named_arguments) + + if not safe_block_is_latest do + # find and fill all events between "safe" and "latest" block (excluding "safe") + {:ok, latest_block} = Optimism.get_block_number_by_tag("latest", json_rpc_named_arguments) + fill_block_range(safe_block + 1, latest_block, message_passer, json_rpc_named_arguments) + end + + {:stop, :normal, state} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + def remove(starting_block) do + Repo.delete_all(from(w in OptimismWithdrawal, where: w.l2_block_number >= ^starting_block)) + end + + def event_to_withdrawal(second_topic, data, l2_transaction_hash, l2_block_number) do + [_value, _gas_limit, _data, hash] = decode_data(data, [{:uint, 256}, {:uint, 256}, :bytes, {:bytes, 32}]) + + msg_nonce = + second_topic + |> Helper.log_topic_to_string() + |> quantity_to_integer() + |> Decimal.new() + + %{ + msg_nonce: msg_nonce, + hash: hash, + l2_transaction_hash: l2_transaction_hash, + l2_block_number: quantity_to_integer(l2_block_number) + } + end + + defp msg_nonce_gap_starts(nonce_max) do + Repo.all( + from(w in OptimismWithdrawal, + select: w.l2_block_number, + order_by: w.msg_nonce, + where: + fragment( + "NOT EXISTS (SELECT msg_nonce FROM op_withdrawals WHERE msg_nonce = (? + 1)) AND msg_nonce != ?", + w.msg_nonce, + ^nonce_max + ) + ) + ) + end + + defp msg_nonce_gap_ends(nonce_min) do + Repo.all( + from(w in OptimismWithdrawal, + select: w.l2_block_number, + order_by: w.msg_nonce, + where: + fragment( + "NOT EXISTS (SELECT msg_nonce FROM op_withdrawals WHERE msg_nonce = (? - 1)) AND msg_nonce != ?", + w.msg_nonce, + ^nonce_min + ) + ) + ) + end + + defp find_and_save_withdrawals( + scan_db, + message_passer, + block_start, + block_end, + json_rpc_named_arguments + ) do + withdrawals = + if scan_db do + query = + from(log in Log, + select: {log.second_topic, log.data, log.transaction_hash, log.block_number}, + where: + log.first_topic == ^@message_passed_event and log.address_hash == ^message_passer and + log.block_number >= ^block_start and log.block_number <= ^block_end + ) + + query + |> Repo.all(timeout: :infinity) + |> Enum.map(fn {second_topic, data, l2_transaction_hash, l2_block_number} -> + event_to_withdrawal(second_topic, data, l2_transaction_hash, l2_block_number) + end) + else + {:ok, result} = + Optimism.get_logs( + block_start, + block_end, + message_passer, + @message_passed_event, + json_rpc_named_arguments, + 3 + ) + + Enum.map(result, fn event -> + event_to_withdrawal( + Enum.at(event["topics"], 1), + event["data"], + event["transactionHash"], + event["blockNumber"] + ) + end) + end + + {:ok, _} = + Chain.import(%{ + optimism_withdrawals: %{params: withdrawals}, + timeout: :infinity + }) + + Enum.count(withdrawals) + end + + defp fill_block_range(l2_block_start, l2_block_end, message_passer, json_rpc_named_arguments, scan_db) do + chunks_number = + if scan_db do + 1 + else + ceil((l2_block_end - l2_block_start + 1) / Optimism.get_logs_range_size()) + end + + chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) + + Enum.reduce(chunk_range, 0, fn current_chunk, withdrawals_count_acc -> + chunk_start = l2_block_start + Optimism.get_logs_range_size() * current_chunk + + chunk_end = + if scan_db do + l2_block_end + else + min(chunk_start + Optimism.get_logs_range_size() - 1, l2_block_end) + end + + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, l2_block_start, l2_block_end, nil, :L2) + + withdrawals_count = + find_and_save_withdrawals( + scan_db, + message_passer, + chunk_start, + chunk_end, + json_rpc_named_arguments + ) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + l2_block_start, + l2_block_end, + "#{withdrawals_count} MessagePassed event(s)", + :L2 + ) + + withdrawals_count_acc + withdrawals_count + end) + end + + defp fill_block_range(start_block, end_block, message_passer, json_rpc_named_arguments) do + fill_block_range(start_block, end_block, message_passer, json_rpc_named_arguments, true) + fill_msg_nonce_gaps(start_block, message_passer, json_rpc_named_arguments, false) + {last_l2_block_number, _} = get_last_l2_item() + fill_block_range(max(start_block, last_l2_block_number), end_block, message_passer, json_rpc_named_arguments, false) + end + + defp fill_msg_nonce_gaps(start_block_l2, message_passer, json_rpc_named_arguments, scan_db \\ true) do + nonce_min = Repo.aggregate(OptimismWithdrawal, :min, :msg_nonce) + nonce_max = Repo.aggregate(OptimismWithdrawal, :max, :msg_nonce) + + with true <- !is_nil(nonce_min) and !is_nil(nonce_max), + starts = msg_nonce_gap_starts(nonce_max), + ends = msg_nonce_gap_ends(nonce_min), + min_block_l2 = l2_block_number_by_msg_nonce(nonce_min), + {new_starts, new_ends} = + if(start_block_l2 < min_block_l2, + do: {[start_block_l2 | starts], [min_block_l2 | ends]}, + else: {starts, ends} + ), + true <- Enum.count(new_starts) == Enum.count(new_ends) do + new_starts + |> Enum.zip(new_ends) + |> Enum.each(fn {l2_block_start, l2_block_end} -> + withdrawals_count = + fill_block_range(l2_block_start, l2_block_end, message_passer, json_rpc_named_arguments, scan_db) + + if withdrawals_count > 0 do + log_fill_msg_nonce_gaps(scan_db, l2_block_start, l2_block_end, withdrawals_count) + end + end) + + if scan_db do + fill_msg_nonce_gaps(start_block_l2, message_passer, json_rpc_named_arguments, false) + end + end + end + + defp get_last_l2_item do + query = + from(w in OptimismWithdrawal, + select: {w.l2_block_number, w.l2_transaction_hash}, + order_by: [desc: w.msg_nonce], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end + + defp log_fill_msg_nonce_gaps(scan_db, l2_block_start, l2_block_end, withdrawals_count) do + find_place = if scan_db, do: "in DB", else: "through RPC" + + Logger.info( + "Filled gaps between L2 blocks #{l2_block_start} and #{l2_block_end}. #{withdrawals_count} event(s) were found #{find_place} and written to op_withdrawals table." + ) + end + + defp l2_block_number_by_msg_nonce(nonce) do + Repo.one(from(w in OptimismWithdrawal, select: w.l2_block_number, where: w.msg_nonce == ^nonce)) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex new file mode 100644 index 000000000000..fdd8bf04d803 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex @@ -0,0 +1,220 @@ +defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do + @moduledoc """ + Fills op_withdrawal_events DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + alias EthereumJSONRPC.Block.ByNumber + alias EthereumJSONRPC.Blocks + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Optimism.WithdrawalEvent + alias Indexer.Fetcher.{Optimism, RollupL1ReorgMonitor} + alias Indexer.Helper + + @fetcher_name :optimism_withdrawal_events + + # 32-byte signature of the event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to) + @withdrawal_proven_event "0x67a6208cfcc0801d50f6cbe764733f4fddf66ac0b04442061a8a8c0cb6b63f62" + + # 32-byte signature of the event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success) + @withdrawal_finalized_event "0xdb5c7652857aa163daadd670e116628fb42e869d8ac4251ef8971d9e5727df1b" + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, :ok}} + end + + @impl GenServer + def handle_continue(:ok, _state) do + Logger.metadata(fetcher: @fetcher_name) + + env = Application.get_all_env(:indexer)[__MODULE__] + optimism_l1_portal = Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism][:optimism_l1_portal] + + Optimism.init_continue(env, optimism_l1_portal, __MODULE__) + end + + @impl GenServer + def handle_info( + :continue, + %{ + contract_address: optimism_portal, + block_check_interval: block_check_interval, + start_block: start_block, + end_block: end_block, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + # credo:disable-for-next-line + time_before = Timex.now() + + chunks_number = ceil((end_block - start_block + 1) / Optimism.get_logs_range_size()) + chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) + + last_written_block = + chunk_range + |> Enum.reduce_while(start_block - 1, fn current_chunk, _ -> + chunk_start = start_block + Optimism.get_logs_range_size() * current_chunk + chunk_end = min(chunk_start + Optimism.get_logs_range_size() - 1, end_block) + + if chunk_end >= chunk_start do + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) + + {:ok, result} = + Optimism.get_logs( + chunk_start, + chunk_end, + optimism_portal, + [@withdrawal_proven_event, @withdrawal_finalized_event], + json_rpc_named_arguments, + Helper.infinite_retries_number() + ) + + withdrawal_events = prepare_events(result, json_rpc_named_arguments) + + {:ok, _} = + Chain.import(%{ + optimism_withdrawal_events: %{params: withdrawal_events}, + timeout: :infinity + }) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(withdrawal_events)} WithdrawalProven/WithdrawalFinalized event(s)", + :L1 + ) + end + + reorg_block = RollupL1ReorgMonitor.reorg_block_pop(__MODULE__) + + if !is_nil(reorg_block) && reorg_block > 0 do + {deleted_count, _} = Repo.delete_all(from(we in WithdrawalEvent, where: we.l1_block_number >= ^reorg_block)) + + log_deleted_rows_count(reorg_block, deleted_count) + + {:halt, if(reorg_block <= chunk_end, do: reorg_block - 1, else: chunk_end)} + else + {:cont, chunk_end} + end + end) + + new_start_block = last_written_block + 1 + + {:ok, new_end_block} = + Optimism.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) + + delay = + if new_end_block == last_written_block do + # there is no new block, so wait for some time to let the chain issue the new block + max(block_check_interval - Timex.diff(Timex.now(), time_before, :milliseconds), 0) + else + 0 + end + + Process.send_after(self(), :continue, delay) + + {:noreply, %{state | start_block: new_start_block, end_block: new_end_block}} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + defp log_deleted_rows_count(reorg_block, count) do + if count > 0 do + Logger.warning( + "As L1 reorg was detected, all rows with l1_block_number >= #{reorg_block} were removed from the op_withdrawal_events table. Number of removed rows: #{count}." + ) + end + end + + defp prepare_events(events, json_rpc_named_arguments) do + timestamps = + events + |> get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number()) + |> Enum.reduce(%{}, fn block, acc -> + block_number = quantity_to_integer(Map.get(block, "number")) + {:ok, timestamp} = DateTime.from_unix(quantity_to_integer(Map.get(block, "timestamp"))) + Map.put(acc, block_number, timestamp) + end) + + Enum.map(events, fn event -> + l1_event_type = + if Enum.at(event["topics"], 0) == @withdrawal_proven_event do + "WithdrawalProven" + else + "WithdrawalFinalized" + end + + l1_block_number = quantity_to_integer(event["blockNumber"]) + + %{ + withdrawal_hash: Enum.at(event["topics"], 1), + l1_event_type: l1_event_type, + l1_timestamp: Map.get(timestamps, l1_block_number), + l1_transaction_hash: event["transactionHash"], + l1_block_number: l1_block_number + } + end) + end + + def get_last_l1_item do + query = + from(we in WithdrawalEvent, + select: {we.l1_block_number, we.l1_transaction_hash}, + order_by: [desc: we.l1_timestamp], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end + + defp get_blocks_by_events(events, json_rpc_named_arguments, retries) do + request = + events + |> Enum.reduce(%{}, fn event, acc -> + Map.put(acc, event["blockNumber"], 0) + end) + |> Stream.map(fn {block_number, _} -> %{number: block_number} end) + |> Stream.with_index() + |> Enum.into(%{}, fn {params, id} -> {id, params} end) + |> Blocks.requests(&ByNumber.request(&1, false, false)) + + error_message = &"Cannot fetch blocks with batch request. Error: #{inspect(&1)}. Request: #{inspect(request)}" + + case Optimism.repeated_request(request, error_message, json_rpc_named_arguments, retries) do + {:ok, results} -> Enum.map(results, fn %{result: result} -> result end) + {:error, _} -> [] + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge.ex index 9365bd58609f..9332e5b8f941 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_edge.ex @@ -3,6 +3,8 @@ defmodule Indexer.Fetcher.PolygonEdge do Contains common functions for PolygonEdge.* fetchers. """ + # todo: this module is deprecated and should be removed + use GenServer use Indexer.Fetcher @@ -11,18 +13,15 @@ defmodule Indexer.Fetcher.PolygonEdge do import Ecto.Query import EthereumJSONRPC, - only: [fetch_block_number_by_tag: 2, json_rpc: 2, integer_to_quantity: 1, quantity_to_integer: 1, request: 1] + only: [json_rpc: 2, integer_to_quantity: 1, request: 1] import Explorer.Helper, only: [parse_integer: 1] - alias EthereumJSONRPC.Block.ByNumber - alias Explorer.Chain.Events.Publisher alias Explorer.{Chain, Repo} - alias Indexer.{BoundQueue, Helper} + alias Indexer.Helper alias Indexer.Fetcher.PolygonEdge.{Deposit, DepositExecute, Withdrawal, WithdrawalExit} @fetcher_name :polygon_edge - @block_check_interval_range_size 100 def child_spec(start_link_arguments) do spec = %{ @@ -42,29 +41,7 @@ defmodule Indexer.Fetcher.PolygonEdge do @impl GenServer def init(_args) do Logger.metadata(fetcher: @fetcher_name) - - modules_using_reorg_monitor = [Deposit, WithdrawalExit] - - reorg_monitor_not_needed = - modules_using_reorg_monitor - |> Enum.all?(fn module -> - is_nil(Application.get_all_env(:indexer)[module][:start_block_l1]) - end) - - if reorg_monitor_not_needed do - :ignore - else - polygon_edge_l1_rpc = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonEdge][:polygon_edge_l1_rpc] - - json_rpc_named_arguments = json_rpc_named_arguments(polygon_edge_l1_rpc) - - {:ok, block_check_interval, _} = get_block_check_interval(json_rpc_named_arguments) - - Process.send(self(), :reorg_monitor, []) - - {:ok, - %{block_check_interval: block_check_interval, json_rpc_named_arguments: json_rpc_named_arguments, prev_latest: 0}} - end + :ignore end @spec init_l1( @@ -79,11 +56,9 @@ defmodule Indexer.Fetcher.PolygonEdge do def init_l1(table, env, pid, contract_address, contract_name, table_name, entity_name) when table in [Explorer.Chain.PolygonEdge.Deposit, Explorer.Chain.PolygonEdge.WithdrawalExit] do with {:start_block_l1_undefined, false} <- {:start_block_l1_undefined, is_nil(env[:start_block_l1])}, - {:reorg_monitor_started, true} <- - {:reorg_monitor_started, !is_nil(Process.whereis(Indexer.Fetcher.PolygonEdge))}, polygon_edge_l1_rpc = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonEdge][:polygon_edge_l1_rpc], {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(polygon_edge_l1_rpc)}, - {:contract_is_valid, true} <- {:contract_is_valid, Helper.is_address_correct?(contract_address)}, + {:contract_is_valid, true} <- {:contract_is_valid, Helper.address_correct?(contract_address)}, start_block_l1 = parse_integer(env[:start_block_l1]), false <- is_nil(start_block_l1), true <- start_block_l1 > 0, @@ -92,10 +67,14 @@ defmodule Indexer.Fetcher.PolygonEdge do {:start_block_l1_valid, start_block_l1 <= last_l1_block_number || last_l1_block_number == 0}, json_rpc_named_arguments = json_rpc_named_arguments(polygon_edge_l1_rpc), {:ok, last_l1_tx} <- - get_transaction_by_hash(last_l1_transaction_hash, json_rpc_named_arguments, 100_000_000), + Helper.get_transaction_by_hash( + last_l1_transaction_hash, + json_rpc_named_arguments, + Helper.infinite_retries_number() + ), {:l1_tx_not_found, false} <- {:l1_tx_not_found, !is_nil(last_l1_transaction_hash) && is_nil(last_l1_tx)}, {:ok, block_check_interval, last_safe_block} <- - get_block_check_interval(json_rpc_named_arguments) do + Helper.get_block_check_interval(json_rpc_named_arguments) do start_block = max(start_block_l1, last_l1_block_number) Process.send(pid, :continue, []) @@ -113,10 +92,6 @@ defmodule Indexer.Fetcher.PolygonEdge do # the process shouldn't start if the start block is not defined :ignore - {:reorg_monitor_started, false} -> - Logger.error("Cannot start this process as reorg monitor in Indexer.Fetcher.PolygonEdge is not started.") - :ignore - {:rpc_l1_undefined, true} -> Logger.error("L1 RPC URL is not defined.") :ignore @@ -163,7 +138,7 @@ defmodule Indexer.Fetcher.PolygonEdge do def init_l2(table, env, pid, contract_address, contract_name, table_name, entity_name, json_rpc_named_arguments) when table in [Explorer.Chain.PolygonEdge.DepositExecute, Explorer.Chain.PolygonEdge.Withdrawal] do with {:start_block_l2_undefined, false} <- {:start_block_l2_undefined, is_nil(env[:start_block_l2])}, - {:contract_address_valid, true} <- {:contract_address_valid, Helper.is_address_correct?(contract_address)}, + {:contract_address_valid, true} <- {:contract_address_valid, Helper.address_correct?(contract_address)}, start_block_l2 = parse_integer(env[:start_block_l2]), false <- is_nil(start_block_l2), true <- start_block_l2 > 0, @@ -172,7 +147,12 @@ defmodule Indexer.Fetcher.PolygonEdge do {:start_block_l2_valid, true} <- {:start_block_l2_valid, (start_block_l2 <= last_l2_block_number || last_l2_block_number == 0) && start_block_l2 <= safe_block}, - {:ok, last_l2_tx} <- get_transaction_by_hash(last_l2_transaction_hash, json_rpc_named_arguments, 100_000_000), + {:ok, last_l2_tx} <- + Helper.get_transaction_by_hash( + last_l2_transaction_hash, + json_rpc_named_arguments, + Helper.infinite_retries_number() + ), {:l2_tx_not_found, false} <- {:l2_tx_not_found, !is_nil(last_l2_transaction_hash) && is_nil(last_l2_tx)} do Process.send(pid, :continue, []) @@ -217,29 +197,7 @@ defmodule Indexer.Fetcher.PolygonEdge do end end - @impl GenServer - def handle_info( - :reorg_monitor, - %{ - block_check_interval: block_check_interval, - json_rpc_named_arguments: json_rpc_named_arguments, - prev_latest: prev_latest - } = state - ) do - {:ok, latest} = get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) - - if latest < prev_latest do - Logger.warning("Reorg detected: previous latest block ##{prev_latest}, current latest block ##{latest}.") - - Publisher.broadcast([{:polygon_edge_reorg_block, latest}], :realtime) - end - - Process.send_after(self(), :reorg_monitor, block_check_interval) - - {:noreply, %{state | prev_latest: latest}} - end - - @spec handle_continue(map(), binary(), Deposit | WithdrawalExit, atom()) :: {:noreply, map()} + @spec handle_continue(map(), binary(), Deposit | WithdrawalExit) :: {:noreply, map()} def handle_continue( %{ contract_address: contract_address, @@ -249,8 +207,7 @@ defmodule Indexer.Fetcher.PolygonEdge do json_rpc_named_arguments: json_rpc_named_arguments } = state, event_signature, - calling_module, - fetcher_name + calling_module ) when calling_module in [Deposit, WithdrawalExit] do time_before = Timex.now() @@ -268,7 +225,7 @@ defmodule Indexer.Fetcher.PolygonEdge do chunk_end = min(chunk_start + eth_get_logs_range_size - 1, end_block) if chunk_end >= chunk_start do - log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, "L1") + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) {:ok, result} = get_logs( @@ -277,7 +234,7 @@ defmodule Indexer.Fetcher.PolygonEdge do contract_address, event_signature, json_rpc_named_arguments, - 100_000_000 + Helper.infinite_retries_number() ) {events, event_name} = @@ -285,28 +242,23 @@ defmodule Indexer.Fetcher.PolygonEdge do |> calling_module.prepare_events(json_rpc_named_arguments) |> import_events(calling_module) - log_blocks_chunk_handling( + Helper.log_blocks_chunk_handling( chunk_start, chunk_end, start_block, end_block, "#{Enum.count(events)} #{event_name} event(s)", - "L1" + :L1 ) end - reorg_block = reorg_block_pop(fetcher_name) - - if !is_nil(reorg_block) && reorg_block > 0 do - reorg_handle(reorg_block, calling_module) - {:halt, if(reorg_block <= chunk_end, do: reorg_block - 1, else: chunk_end)} - else - {:cont, chunk_end} - end + {:cont, chunk_end} end) new_start_block = last_written_block + 1 - {:ok, new_end_block} = get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + + {:ok, new_end_block} = + Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) delay = if new_end_block == last_written_block do @@ -356,7 +308,7 @@ defmodule Indexer.Fetcher.PolygonEdge do min(chunk_start + eth_get_logs_range_size - 1, l2_block_end) end - log_blocks_chunk_handling(chunk_start, chunk_end, l2_block_start, l2_block_end, nil, "L2") + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, l2_block_start, l2_block_end, nil, :L2) count = calling_module.find_and_save_entities( @@ -374,13 +326,13 @@ defmodule Indexer.Fetcher.PolygonEdge do "L2StateSynced" end - log_blocks_chunk_handling( + Helper.log_blocks_chunk_handling( chunk_start, chunk_end, l2_block_start, l2_block_end, "#{count} #{event_name} event(s)", - "L2" + :L2 ) count_acc + count @@ -540,66 +492,15 @@ defmodule Indexer.Fetcher.PolygonEdge do Repo.all(query) end - defp get_block_check_interval(json_rpc_named_arguments) do - {last_safe_block, _} = get_safe_block(json_rpc_named_arguments) - - first_block = max(last_safe_block - @block_check_interval_range_size, 1) - - with {:ok, first_block_timestamp} <- - get_block_timestamp_by_number(first_block, json_rpc_named_arguments, 100_000_000), - {:ok, last_safe_block_timestamp} <- - get_block_timestamp_by_number(last_safe_block, json_rpc_named_arguments, 100_000_000) do - block_check_interval = - ceil((last_safe_block_timestamp - first_block_timestamp) / (last_safe_block - first_block) * 1000 / 2) - - Logger.info("Block check interval is calculated as #{block_check_interval} ms.") - {:ok, block_check_interval, last_safe_block} - else - {:error, error} -> - {:error, "Failed to calculate block check interval due to #{inspect(error)}"} - end - end - - @spec get_block_number_by_tag(binary(), list(), integer()) :: {:ok, non_neg_integer()} | {:error, atom()} - def get_block_number_by_tag(tag, json_rpc_named_arguments, retries \\ 3) do - error_message = &"Cannot fetch #{tag} block number. Error: #{inspect(&1)}" - repeated_call(&fetch_block_number_by_tag/2, [tag, json_rpc_named_arguments], error_message, retries) - end - - defp get_block_timestamp_by_number_inner(number, json_rpc_named_arguments) do - result = - %{id: 0, number: number} - |> ByNumber.request(false) - |> json_rpc(json_rpc_named_arguments) - - with {:ok, block} <- result, - false <- is_nil(block), - timestamp <- Map.get(block, "timestamp"), - false <- is_nil(timestamp) do - {:ok, quantity_to_integer(timestamp)} - else - {:error, message} -> - {:error, message} - - true -> - {:error, "RPC returned nil."} - end - end - - defp get_block_timestamp_by_number(number, json_rpc_named_arguments, retries) do - func = &get_block_timestamp_by_number_inner/2 - args = [number, json_rpc_named_arguments] - error_message = &"Cannot fetch block ##{number} or its timestamp. Error: #{inspect(&1)}" - repeated_call(func, args, error_message, retries) - end - defp get_safe_block(json_rpc_named_arguments) do - case get_block_number_by_tag("safe", json_rpc_named_arguments) do + case Helper.get_block_number_by_tag("safe", json_rpc_named_arguments) do {:ok, safe_block} -> {safe_block, false} {:error, :not_found} -> - {:ok, latest_block} = get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + {:ok, latest_block} = + Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) + {latest_block, true} end end @@ -632,22 +533,7 @@ defmodule Indexer.Fetcher.PolygonEdge do error_message = &"Cannot fetch logs for the block range #{from_block}..#{to_block}. Error: #{inspect(&1)}" - repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) - end - - defp get_transaction_by_hash(hash, _json_rpc_named_arguments, _retries_left) when is_nil(hash), do: {:ok, nil} - - defp get_transaction_by_hash(hash, json_rpc_named_arguments, retries) do - req = - request(%{ - id: 0, - method: "eth_getTransactionByHash", - params: [hash] - }) - - error_message = &"eth_getTransactionByHash failed. Error: #{inspect(&1)}" - - repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) end defp get_last_l1_item(table) do @@ -696,50 +582,18 @@ defmodule Indexer.Fetcher.PolygonEdge do Repo.one(from(item in table, select: item.l2_block_number, where: item.msg_id == ^id)) end - defp log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, items_count, layer) do - is_start = is_nil(items_count) - - {type, found} = - if is_start do - {"Start", ""} - else - {"Finish", " Found #{items_count}."} - end - - target_range = - if chunk_start != start_block or chunk_end != end_block do - progress = - if is_start do - "" - else - percentage = - (chunk_end - start_block + 1) - |> Decimal.div(end_block - start_block + 1) - |> Decimal.mult(100) - |> Decimal.round(2) - |> Decimal.to_string() - - " Progress: #{percentage}%" - end - - " Target range: #{start_block}..#{end_block}.#{progress}" - else - "" - end - - if chunk_start == chunk_end do - Logger.info("#{type} handling #{layer} block ##{chunk_start}.#{found}#{target_range}") - else - Logger.info("#{type} handling #{layer} block range #{chunk_start}..#{chunk_end}.#{found}#{target_range}") - end - end - defp import_events(events, calling_module) do + # here we explicitly check CHAIN_TYPE as Dialyzer throws an error otherwise {import_data, event_name} = - if calling_module == Deposit do - {%{polygon_edge_deposits: %{params: events}, timeout: :infinity}, "StateSynced"} - else - {%{polygon_edge_withdrawal_exits: %{params: events}, timeout: :infinity}, "ExitProcessed"} + case Application.get_env(:explorer, :chain_type) == "polygon_edge" && calling_module do + Deposit -> + {%{polygon_edge_deposits: %{params: events}, timeout: :infinity}, "StateSynced"} + + WithdrawalExit -> + {%{polygon_edge_withdrawal_exits: %{params: events}, timeout: :infinity}, "ExitProcessed"} + + _ -> + {%{}, ""} end {:ok, _} = Chain.import(import_data) @@ -747,91 +601,8 @@ defmodule Indexer.Fetcher.PolygonEdge do {events, event_name} end - defp log_deleted_rows_count(reorg_block, count, table_name) do - if count > 0 do - Logger.warning( - "As L1 reorg was detected, all rows with l1_block_number >= #{reorg_block} were removed from the #{table_name} table. Number of removed rows: #{count}." - ) - end - end - - defp repeated_call(func, args, error_message, retries_left) do - case apply(func, args) do - {:ok, _} = res -> - res - - {:error, message} = err -> - retries_left = retries_left - 1 - - if retries_left <= 0 do - Logger.error(error_message.(message)) - err - else - Logger.error("#{error_message.(message)} Retrying...") - :timer.sleep(3000) - repeated_call(func, args, error_message, retries_left) - end - end - end - @spec repeated_request(list(), any(), list(), non_neg_integer()) :: {:ok, any()} | {:error, atom()} def repeated_request(req, error_message, json_rpc_named_arguments, retries) do - repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) - end - - defp reorg_block_pop(fetcher_name) do - table_name = reorg_table_name(fetcher_name) - - case BoundQueue.pop_front(reorg_queue_get(table_name)) do - {:ok, {block_number, updated_queue}} -> - :ets.insert(table_name, {:queue, updated_queue}) - block_number - - {:error, :empty} -> - nil - end - end - - @spec reorg_block_push(atom(), non_neg_integer()) :: no_return() - def reorg_block_push(fetcher_name, block_number) do - table_name = reorg_table_name(fetcher_name) - {:ok, updated_queue} = BoundQueue.push_back(reorg_queue_get(table_name), block_number) - :ets.insert(table_name, {:queue, updated_queue}) - end - - defp reorg_handle(reorg_block, calling_module) do - {table, table_name} = - if calling_module == Deposit do - {Explorer.Chain.PolygonEdge.Deposit, "polygon_edge_deposits"} - else - {Explorer.Chain.PolygonEdge.WithdrawalExit, "polygon_edge_withdrawal_exits"} - end - - {deleted_count, _} = Repo.delete_all(from(item in table, where: item.l1_block_number >= ^reorg_block)) - - log_deleted_rows_count(reorg_block, deleted_count, table_name) - end - - defp reorg_queue_get(table_name) do - if :ets.whereis(table_name) == :undefined do - :ets.new(table_name, [ - :set, - :named_table, - :public, - read_concurrency: true, - write_concurrency: true - ]) - end - - with info when info != :undefined <- :ets.info(table_name), - [{_, value}] <- :ets.lookup(table_name, :queue) do - value - else - _ -> %BoundQueue{} - end - end - - defp reorg_table_name(fetcher_name) do - :"#{fetcher_name}#{:_reorgs}" + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) end end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex index ca11c30e08c8..556acfd892a6 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex @@ -3,6 +3,8 @@ defmodule Indexer.Fetcher.PolygonEdge.Deposit do Fills polygon_edge_deposits DB table. """ + # todo: this module is deprecated and should be removed + use GenServer use Indexer.Fetcher @@ -14,9 +16,9 @@ defmodule Indexer.Fetcher.PolygonEdge.Deposit do alias ABI.TypeDecoder alias EthereumJSONRPC.Block.ByNumber alias EthereumJSONRPC.Blocks - alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.PolygonEdge.Deposit alias Indexer.Fetcher.PolygonEdge + alias Indexer.Helper @fetcher_name :polygon_edge_deposit @@ -47,8 +49,6 @@ defmodule Indexer.Fetcher.PolygonEdge.Deposit do env = Application.get_all_env(:indexer)[__MODULE__] - Subscriber.to(:polygon_edge_reorg_block, :realtime) - PolygonEdge.init_l1( Deposit, env, @@ -62,13 +62,7 @@ defmodule Indexer.Fetcher.PolygonEdge.Deposit do @impl GenServer def handle_info(:continue, state) do - PolygonEdge.handle_continue(state, @state_synced_event, __MODULE__, @fetcher_name) - end - - @impl GenServer - def handle_info({:chain_event, :polygon_edge_reorg_block, :realtime, block_number}, state) do - PolygonEdge.reorg_block_push(@fetcher_name, block_number) - {:noreply, state} + PolygonEdge.handle_continue(state, @state_synced_event, __MODULE__) end @impl GenServer @@ -130,7 +124,7 @@ defmodule Indexer.Fetcher.PolygonEdge.Deposit do defp get_timestamps_by_events(events, json_rpc_named_arguments) do events - |> get_blocks_by_events(json_rpc_named_arguments, 100_000_000) + |> get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number()) |> Enum.reduce(%{}, fn block, acc -> block_number = quantity_to_integer(Map.get(block, "number")) {:ok, timestamp} = DateTime.from_unix(quantity_to_integer(Map.get(block, "timestamp"))) diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex index 4e2dd57d09eb..8c1c85c220d1 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex @@ -3,6 +3,8 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do Fills polygon_edge_deposit_executes DB table. """ + # todo: this module is deprecated and should be removed + use GenServer use Indexer.Fetcher @@ -11,12 +13,14 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do import Ecto.Query import EthereumJSONRPC, only: [quantity_to_integer: 1] - import Indexer.Fetcher.PolygonEdge, only: [fill_block_range: 5, get_block_number_by_tag: 3] + import Indexer.Fetcher.PolygonEdge, only: [fill_block_range: 5] + import Indexer.Helper, only: [log_topic_to_string: 1] alias Explorer.{Chain, Repo} alias Explorer.Chain.Log alias Explorer.Chain.PolygonEdge.DepositExecute alias Indexer.Fetcher.PolygonEdge + alias Indexer.Helper @fetcher_name :polygon_edge_deposit_execute @@ -101,7 +105,8 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do if not safe_block_is_latest do # find and fill all events between "safe" and "latest" block (excluding "safe") - {:ok, latest_block} = get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + {:ok, latest_block} = + Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) fill_block_range( safe_block + 1, @@ -128,11 +133,21 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do @spec event_to_deposit_execute(binary(), binary(), binary(), binary()) :: map() def event_to_deposit_execute(second_topic, third_topic, l2_transaction_hash, l2_block_number) do + msg_id = + second_topic + |> log_topic_to_string() + |> quantity_to_integer() + + status = + third_topic + |> log_topic_to_string() + |> quantity_to_integer() + %{ - msg_id: quantity_to_integer(second_topic), + msg_id: msg_id, l2_transaction_hash: l2_transaction_hash, l2_block_number: quantity_to_integer(l2_block_number), - success: quantity_to_integer(third_topic) != 0 + success: status != 0 } end @@ -150,7 +165,7 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do from(log in Log, select: {log.second_topic, log.third_topic, log.transaction_hash, log.block_number}, where: - log.first_topic == @state_sync_result_event and log.address_hash == ^state_receiver and + log.first_topic == ^@state_sync_result_event and log.address_hash == ^state_receiver and log.block_number >= ^block_start and log.block_number <= ^block_end ) @@ -167,7 +182,7 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do state_receiver, @state_sync_result_event, json_rpc_named_arguments, - 100_000_000 + Helper.infinite_retries_number() ) Enum.map(result, fn event -> @@ -180,11 +195,18 @@ defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do end) end - {:ok, _} = - Chain.import(%{ - polygon_edge_deposit_executes: %{params: executes}, - timeout: :infinity - }) + # here we explicitly check CHAIN_TYPE as Dialyzer throws an error otherwise + import_options = + if Application.get_env(:explorer, :chain_type) == "polygon_edge" do + %{ + polygon_edge_deposit_executes: %{params: executes}, + timeout: :infinity + } + else + %{} + end + + {:ok, _} = Chain.import(import_options) Enum.count(executes) end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex index 80fb20568c99..4ec0f7e0776c 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex @@ -3,6 +3,8 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do Fills polygon_edge_withdrawals DB table. """ + # todo: this module is deprecated and should be removed + use GenServer use Indexer.Fetcher @@ -12,13 +14,15 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do import EthereumJSONRPC, only: [quantity_to_integer: 1] import Explorer.Helper, only: [decode_data: 2] - import Indexer.Fetcher.PolygonEdge, only: [fill_block_range: 5, get_block_number_by_tag: 3] + import Indexer.Fetcher.PolygonEdge, only: [fill_block_range: 5] + import Indexer.Helper, only: [log_topic_to_string: 1] alias ABI.TypeDecoder alias Explorer.{Chain, Repo} alias Explorer.Chain.Log alias Explorer.Chain.PolygonEdge.Withdrawal alias Indexer.Fetcher.PolygonEdge + alias Indexer.Helper @fetcher_name :polygon_edge_withdrawal @@ -106,7 +110,8 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do if not safe_block_is_latest do # find and fill all events between "safe" and "latest" block (excluding "safe") - {:ok, latest_block} = get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + {:ok, latest_block} = + Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) fill_block_range( safe_block + 1, @@ -133,6 +138,11 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do @spec event_to_withdrawal(binary(), map(), binary(), binary()) :: map() def event_to_withdrawal(second_topic, data, l2_transaction_hash, l2_block_number) do + msg_id = + second_topic + |> log_topic_to_string() + |> quantity_to_integer() + [data_bytes] = decode_data(data, [:bytes]) sig = binary_part(data_bytes, 0, 32) @@ -148,7 +158,7 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do end %{ - msg_id: quantity_to_integer(second_topic), + msg_id: msg_id, from: from, to: to, l2_transaction_hash: l2_transaction_hash, @@ -170,7 +180,7 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do from(log in Log, select: {log.second_topic, log.data, log.transaction_hash, log.block_number}, where: - log.first_topic == @l2_state_synced_event and log.address_hash == ^state_sender and + log.first_topic == ^@l2_state_synced_event and log.address_hash == ^state_sender and log.block_number >= ^block_start and log.block_number <= ^block_end ) @@ -187,7 +197,7 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do state_sender, @l2_state_synced_event, json_rpc_named_arguments, - 100_000_000 + Helper.infinite_retries_number() ) Enum.map(result, fn event -> @@ -200,11 +210,18 @@ defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do end) end - {:ok, _} = - Chain.import(%{ - polygon_edge_withdrawals: %{params: withdrawals}, - timeout: :infinity - }) + # here we explicitly check CHAIN_TYPE as Dialyzer throws an error otherwise + import_options = + if Application.get_env(:explorer, :chain_type) == "polygon_edge" do + %{ + polygon_edge_withdrawals: %{params: withdrawals}, + timeout: :infinity + } + else + %{} + end + + {:ok, _} = Chain.import(import_options) Enum.count(withdrawals) end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex index 5b41e122ddc8..e19ea6517cf1 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex @@ -3,6 +3,8 @@ defmodule Indexer.Fetcher.PolygonEdge.WithdrawalExit do Fills polygon_edge_withdrawal_exits DB table. """ + # todo: this module is deprecated and should be removed + use GenServer use Indexer.Fetcher @@ -10,7 +12,6 @@ defmodule Indexer.Fetcher.PolygonEdge.WithdrawalExit do import EthereumJSONRPC, only: [quantity_to_integer: 1] - alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.PolygonEdge.WithdrawalExit alias Indexer.Fetcher.PolygonEdge @@ -40,8 +41,6 @@ defmodule Indexer.Fetcher.PolygonEdge.WithdrawalExit do env = Application.get_all_env(:indexer)[__MODULE__] - Subscriber.to(:polygon_edge_reorg_block, :realtime) - PolygonEdge.init_l1( WithdrawalExit, env, @@ -55,13 +54,7 @@ defmodule Indexer.Fetcher.PolygonEdge.WithdrawalExit do @impl GenServer def handle_info(:continue, state) do - PolygonEdge.handle_continue(state, @exit_processed_event, __MODULE__, @fetcher_name) - end - - @impl GenServer - def handle_info({:chain_event, :polygon_edge_reorg_block, :realtime, block_number}, state) do - PolygonEdge.reorg_block_push(@fetcher_name, block_number) - {:noreply, state} + PolygonEdge.handle_continue(state, @exit_processed_event, __MODULE__) end @impl GenServer diff --git a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex new file mode 100644 index 000000000000..b38807a5d5b4 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex @@ -0,0 +1,413 @@ +defmodule Indexer.Fetcher.PolygonZkevm.Bridge do + @moduledoc """ + Contains common functions for Indexer.Fetcher.PolygonZkevm.Bridge* modules. + """ + + require Logger + + import EthereumJSONRPC, + only: [ + integer_to_quantity: 1, + json_rpc: 2, + quantity_to_integer: 1, + request: 1, + timestamp_to_datetime: 1 + ] + + import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] + + import Explorer.Helper, only: [decode_data: 2] + + alias EthereumJSONRPC.Logs + alias Explorer.Chain + alias Explorer.Chain.PolygonZkevm.Reader + alias Explorer.SmartContract.Reader, as: SmartContractReader + alias Indexer.Helper + alias Indexer.Transform.Addresses + + # 32-byte signature of the event BridgeEvent(uint8 leafType, uint32 originNetwork, address originAddress, uint32 destinationNetwork, address destinationAddress, uint256 amount, bytes metadata, uint32 depositCount) + @bridge_event "0x501781209a1f8899323b96b4ef08b168df93e0a90c673d1e4cce39366cb62f9b" + @bridge_event_params [{:uint, 8}, {:uint, 32}, :address, {:uint, 32}, :address, {:uint, 256}, :bytes, {:uint, 32}] + + # 32-byte signature of the event ClaimEvent(uint32 index, uint32 originNetwork, address originAddress, address destinationAddress, uint256 amount) + @claim_event "0x25308c93ceeed162da955b3f7ce3e3f93606579e40fb92029faa9efe27545983" + @claim_event_params [{:uint, 32}, {:uint, 32}, :address, :address, {:uint, 256}] + + @symbol_method_selector "95d89b41" + @decimals_method_selector "313ce567" + + @erc20_abi [ + %{ + "constant" => true, + "inputs" => [], + "name" => "symbol", + "outputs" => [%{"name" => "", "type" => "string"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "decimals", + "outputs" => [%{"name" => "", "type" => "uint8"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + + @doc """ + Filters the given list of events keeping only `BridgeEvent` and `ClaimEvent` ones + emitted by the bridge contract. + """ + @spec filter_bridge_events(list(), binary()) :: list() + def filter_bridge_events(events, bridge_contract) do + Enum.filter(events, fn event -> + Helper.address_hash_to_string(event.address_hash, true) == bridge_contract and + Enum.member?([@bridge_event, @claim_event], Helper.log_topic_to_string(event.first_topic)) + end) + end + + @doc """ + Fetches `BridgeEvent` and `ClaimEvent` events of the bridge contract from an RPC node + for the given range of blocks. + """ + @spec get_logs_all({non_neg_integer(), non_neg_integer()}, binary(), list()) :: list() + def get_logs_all({chunk_start, chunk_end}, bridge_contract, json_rpc_named_arguments) do + {:ok, result} = + get_logs( + chunk_start, + chunk_end, + bridge_contract, + [[@bridge_event, @claim_event]], + json_rpc_named_arguments + ) + + Logs.elixir_to_params(result) + end + + defp get_logs(from_block, to_block, address, topics, json_rpc_named_arguments, retries \\ 100_000_000) do + processed_from_block = if is_integer(from_block), do: integer_to_quantity(from_block), else: from_block + processed_to_block = if is_integer(to_block), do: integer_to_quantity(to_block), else: to_block + + req = + request(%{ + id: 0, + method: "eth_getLogs", + params: [ + %{ + :fromBlock => processed_from_block, + :toBlock => processed_to_block, + :address => address, + :topics => topics + } + ] + }) + + error_message = &"Cannot fetch logs for the block range #{from_block}..#{to_block}. Error: #{inspect(&1)}" + + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + end + + @doc """ + Imports the given zkEVM bridge operations into database. + Used by Indexer.Fetcher.PolygonZkevm.BridgeL1 and Indexer.Fetcher.PolygonZkevm.BridgeL2 fetchers. + Doesn't return anything. + """ + @spec import_operations(list()) :: no_return() + def import_operations(operations) do + addresses = + Addresses.extract_addresses(%{ + polygon_zkevm_bridge_operations: operations + }) + + {:ok, _} = + Chain.import(%{ + addresses: %{params: addresses, on_conflict: :nothing}, + polygon_zkevm_bridge_operations: %{params: operations}, + timeout: :infinity + }) + end + + @doc """ + Converts the list of zkEVM bridge events to the list of operations + preparing them for importing to the database. + """ + @spec prepare_operations(list(), list() | nil, list(), map() | nil) :: list() + def prepare_operations(events, json_rpc_named_arguments, json_rpc_named_arguments_l1, block_to_timestamp \\ nil) do + {block_to_timestamp, token_address_to_id} = + if is_nil(block_to_timestamp) do + bridge_events = Enum.filter(events, fn event -> event.first_topic == @bridge_event end) + + l1_token_addresses = + bridge_events + |> Enum.reduce(%MapSet{}, fn event, acc -> + case bridge_event_parse(event) do + {{nil, _}, _, _} -> acc + {{token_address, nil}, _, _} -> MapSet.put(acc, token_address) + end + end) + |> MapSet.to_list() + + { + blocks_to_timestamps(bridge_events, json_rpc_named_arguments), + token_addresses_to_ids(l1_token_addresses, json_rpc_named_arguments_l1) + } + else + # this is called in realtime + {block_to_timestamp, %{}} + end + + Enum.map(events, fn event -> + {index, l1_token_id, l1_token_address, l2_token_address, amount, block_number, block_timestamp} = + if event.first_topic == @bridge_event do + { + {l1_token_address, l2_token_address}, + amount, + deposit_count + } = bridge_event_parse(event) + + l1_token_id = Map.get(token_address_to_id, l1_token_address) + block_number = quantity_to_integer(event.block_number) + block_timestamp = Map.get(block_to_timestamp, block_number) + + # credo:disable-for-lines:2 Credo.Check.Refactor.Nesting + l1_token_address = + if is_nil(l1_token_id) do + l1_token_address + end + + {deposit_count, l1_token_id, l1_token_address, l2_token_address, amount, block_number, block_timestamp} + else + [index, _origin_network, _origin_address, _destination_address, amount] = + decode_data(event.data, @claim_event_params) + + {index, nil, nil, nil, amount, nil, nil} + end + + is_l1 = json_rpc_named_arguments == json_rpc_named_arguments_l1 + + result = %{ + type: operation_type(event.first_topic, is_l1), + index: index, + amount: amount + } + + transaction_hash_field = + if is_l1 do + :l1_transaction_hash + else + :l2_transaction_hash + end + + result + |> extend_result(transaction_hash_field, event.transaction_hash) + |> extend_result(:l1_token_id, l1_token_id) + |> extend_result(:l1_token_address, l1_token_address) + |> extend_result(:l2_token_address, l2_token_address) + |> extend_result(:block_number, block_number) + |> extend_result(:block_timestamp, block_timestamp) + end) + end + + defp blocks_to_timestamps(events, json_rpc_named_arguments) do + events + |> Helper.get_blocks_by_events(json_rpc_named_arguments, 100_000_000) + |> Enum.reduce(%{}, fn block, acc -> + block_number = quantity_to_integer(Map.get(block, "number")) + timestamp = timestamp_to_datetime(Map.get(block, "timestamp")) + Map.put(acc, block_number, timestamp) + end) + end + + defp bridge_event_parse(event) do + [ + leaf_type, + origin_network, + origin_address, + _destination_network, + _destination_address, + amount, + _metadata, + deposit_count + ] = decode_data(event.data, @bridge_event_params) + + {token_address_by_origin_address(origin_address, origin_network, leaf_type), amount, deposit_count} + end + + defp operation_type(first_topic, is_l1) do + if first_topic == @bridge_event do + if is_l1, do: :deposit, else: :withdrawal + else + if is_l1, do: :withdrawal, else: :deposit + end + end + + @doc """ + Fetches L1 token data for the given token addresses, + builds `L1 token address -> L1 token id` map for them, + and writes the data to the database. Returns the resulting map. + """ + @spec token_addresses_to_ids(list(), list()) :: map() + def token_addresses_to_ids(l1_token_addresses, json_rpc_named_arguments) do + token_data = + l1_token_addresses + |> get_token_data(json_rpc_named_arguments) + + tokens_existing = + token_data + |> Map.keys() + |> Reader.token_addresses_to_ids_from_db() + + tokens_to_insert = + token_data + |> Enum.reject(fn {address, _} -> Map.has_key?(tokens_existing, address) end) + |> Enum.map(fn {address, data} -> Map.put(data, :address, address) end) + + {:ok, inserts} = + Chain.import(%{ + polygon_zkevm_bridge_l1_tokens: %{params: tokens_to_insert}, + timeout: :infinity + }) + + tokens_inserted = Map.get(inserts, :insert_polygon_zkevm_bridge_l1_tokens, []) + + # we need to query not inserted tokens from DB separately as they + # could be inserted by another module at the same time (a race condition). + # this is an unlikely case but we handle it here as well + tokens_not_inserted = + tokens_to_insert + |> Enum.reject(fn token -> + Enum.any?(tokens_inserted, fn inserted -> token.address == Helper.address_hash_to_string(inserted.address) end) + end) + |> Enum.map(& &1.address) + + tokens_inserted_outside = Reader.token_addresses_to_ids_from_db(tokens_not_inserted) + + tokens_inserted + |> Enum.reduce(%{}, fn t, acc -> Map.put(acc, Helper.address_hash_to_string(t.address), t.id) end) + |> Map.merge(tokens_existing) + |> Map.merge(tokens_inserted_outside) + end + + defp token_address_by_origin_address(origin_address, origin_network, leaf_type) do + with true <- leaf_type != 1 and origin_network <= 1, + token_address = "0x" <> Base.encode16(origin_address, case: :lower), + true <- token_address != burn_address_hash_string() do + if origin_network == 0 do + # this is L1 address + {token_address, nil} + else + # this is L2 address + {nil, token_address} + end + else + _ -> {nil, nil} + end + end + + defp get_token_data(token_addresses, json_rpc_named_arguments) do + # first, we're trying to read token data from the DB. + # if tokens are not in the DB, read them through RPC. + token_addresses + |> Reader.get_token_data_from_db() + |> get_token_data_from_rpc(json_rpc_named_arguments) + end + + defp get_token_data_from_rpc({token_data, token_addresses}, json_rpc_named_arguments) do + {requests, responses} = get_token_data_request_symbol_decimals(token_addresses, json_rpc_named_arguments) + + requests + |> Enum.zip(responses) + |> Enum.reduce(token_data, fn {request, {status, response} = _resp}, token_data_acc -> + if status == :ok do + response = parse_response(response) + + address = Helper.address_hash_to_string(request.contract_address, true) + + new_data = get_new_data(token_data_acc[address] || %{}, request, response) + + Map.put(token_data_acc, address, new_data) + else + token_data_acc + end + end) + end + + defp get_token_data_request_symbol_decimals(token_addresses, json_rpc_named_arguments) do + requests = + token_addresses + |> Enum.map(fn address -> + # we will call symbol() and decimals() public getters + Enum.map([@symbol_method_selector, @decimals_method_selector], fn method_id -> + %{ + contract_address: address, + method_id: method_id, + args: [] + } + end) + end) + |> List.flatten() + + {responses, error_messages} = read_contracts_with_retries(requests, @erc20_abi, json_rpc_named_arguments, 3) + + if !Enum.empty?(error_messages) or Enum.count(requests) != Enum.count(responses) do + Logger.warning( + "Cannot read symbol and decimals of an ERC-20 token contract. Error messages: #{Enum.join(error_messages, ", ")}. Addresses: #{Enum.join(token_addresses, ", ")}" + ) + end + + {requests, responses} + end + + defp read_contracts_with_retries(requests, abi, json_rpc_named_arguments, retries_left) when retries_left > 0 do + responses = SmartContractReader.query_contracts(requests, abi, json_rpc_named_arguments: json_rpc_named_arguments) + + error_messages = + Enum.reduce(responses, [], fn {status, error_message}, acc -> + acc ++ + if status == :error do + [error_message] + else + [] + end + end) + + if Enum.empty?(error_messages) do + {responses, []} + else + retries_left = retries_left - 1 + + if retries_left == 0 do + {responses, Enum.uniq(error_messages)} + else + :timer.sleep(1000) + read_contracts_with_retries(requests, abi, json_rpc_named_arguments, retries_left) + end + end + end + + defp get_new_data(data, request, response) do + if atomized_key(request.method_id) == :symbol do + Map.put(data, :symbol, response) + else + Map.put(data, :decimals, response) + end + end + + defp extend_result(result, _key, value) when is_nil(value), do: result + defp extend_result(result, key, value) when is_atom(key), do: Map.put(result, key, value) + + defp atomized_key("symbol"), do: :symbol + defp atomized_key("decimals"), do: :decimals + defp atomized_key(@symbol_method_selector), do: :symbol + defp atomized_key(@decimals_method_selector), do: :decimals + + defp parse_response(response) do + case response do + [item] -> item + items -> items + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l1.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l1.ex new file mode 100644 index 000000000000..9899c709910a --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l1.ex @@ -0,0 +1,210 @@ +defmodule Indexer.Fetcher.PolygonZkevm.BridgeL1 do + @moduledoc """ + Fills polygon_zkevm_bridge DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + import Explorer.Helper, only: [parse_integer: 1] + + import Indexer.Fetcher.PolygonZkevm.Bridge, + only: [get_logs_all: 3, import_operations: 1, prepare_operations: 3] + + alias Explorer.Chain.PolygonZkevm.{Bridge, Reader} + alias Explorer.Repo + alias Indexer.Fetcher.RollupL1ReorgMonitor + alias Indexer.Helper + + @eth_get_logs_range_size 1000 + @fetcher_name :polygon_zkevm_bridge_l1 + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, :ok}} + end + + @impl GenServer + def handle_continue(_, state) do + Logger.metadata(fetcher: @fetcher_name) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + Process.send_after(self(), :init_with_delay, 2000) + {:noreply, state} + end + + @impl GenServer + def handle_info(:init_with_delay, _state) do + env = Application.get_all_env(:indexer)[__MODULE__] + + with {:start_block_undefined, false} <- {:start_block_undefined, is_nil(env[:start_block])}, + {:reorg_monitor_started, true} <- {:reorg_monitor_started, !is_nil(Process.whereis(RollupL1ReorgMonitor))}, + rpc = env[:rpc], + {:rpc_undefined, false} <- {:rpc_undefined, is_nil(rpc)}, + {:bridge_contract_address_is_valid, true} <- + {:bridge_contract_address_is_valid, Helper.address_correct?(env[:bridge_contract])}, + start_block = parse_integer(env[:start_block]), + false <- is_nil(start_block), + true <- start_block > 0, + {last_l1_block_number, last_l1_transaction_hash} = Reader.last_l1_item(), + json_rpc_named_arguments = Helper.json_rpc_named_arguments(rpc), + {:ok, block_check_interval, safe_block} <- Helper.get_block_check_interval(json_rpc_named_arguments), + {:start_block_valid, true, _, _} <- + {:start_block_valid, + (start_block <= last_l1_block_number || last_l1_block_number == 0) && start_block <= safe_block, + last_l1_block_number, safe_block}, + {:ok, last_l1_tx} <- Helper.get_transaction_by_hash(last_l1_transaction_hash, json_rpc_named_arguments), + {:l1_tx_not_found, false} <- {:l1_tx_not_found, !is_nil(last_l1_transaction_hash) && is_nil(last_l1_tx)} do + Process.send(self(), :continue, []) + + {:noreply, + %{ + block_check_interval: block_check_interval, + bridge_contract: env[:bridge_contract], + json_rpc_named_arguments: json_rpc_named_arguments, + end_block: safe_block, + start_block: max(start_block, last_l1_block_number) + }} + else + {:start_block_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, %{}} + + {:reorg_monitor_started, false} -> + Logger.error("Cannot start this process as Indexer.Fetcher.RollupL1ReorgMonitor is not started.") + {:stop, :normal, %{}} + + {:rpc_undefined, true} -> + Logger.error("L1 RPC URL is not defined.") + {:stop, :normal, %{}} + + {:bridge_contract_address_is_valid, false} -> + Logger.error("PolygonZkEVMBridge contract address is invalid or not defined.") + {:stop, :normal, %{}} + + {:start_block_valid, false, last_l1_block_number, safe_block} -> + Logger.error("Invalid L1 Start Block value. Please, check the value and polygon_zkevm_bridge table.") + Logger.error("last_l1_block_number = #{inspect(last_l1_block_number)}") + Logger.error("safe_block = #{inspect(safe_block)}") + {:stop, :normal, %{}} + + {:error, error_data} -> + Logger.error( + "Cannot get last L1 transaction from RPC by its hash, latest block, or block timestamp by its number due to RPC error: #{inspect(error_data)}" + ) + + {:stop, :normal, %{}} + + {:l1_tx_not_found, true} -> + Logger.error( + "Cannot find last L1 transaction from RPC by its hash. Probably, there was a reorg on L1 chain. Please, check polygon_zkevm_bridge table." + ) + + {:stop, :normal, %{}} + + _ -> + Logger.error("L1 Start Block is invalid or zero.") + {:stop, :normal, %{}} + end + end + + @impl GenServer + def handle_info( + :continue, + %{ + bridge_contract: bridge_contract, + block_check_interval: block_check_interval, + start_block: start_block, + end_block: end_block, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + time_before = Timex.now() + + last_written_block = + start_block..end_block + |> Enum.chunk_every(@eth_get_logs_range_size) + |> Enum.reduce_while(start_block - 1, fn current_chunk, _ -> + chunk_start = List.first(current_chunk) + chunk_end = List.last(current_chunk) + + if chunk_start <= chunk_end do + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) + + operations = + {chunk_start, chunk_end} + |> get_logs_all(bridge_contract, json_rpc_named_arguments) + |> prepare_operations(json_rpc_named_arguments, json_rpc_named_arguments) + + import_operations(operations) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(operations)} L1 operation(s)", + :L1 + ) + end + + reorg_block = RollupL1ReorgMonitor.reorg_block_pop(__MODULE__) + + if !is_nil(reorg_block) && reorg_block > 0 do + reorg_handle(reorg_block) + {:halt, if(reorg_block <= chunk_end, do: reorg_block - 1, else: chunk_end)} + else + {:cont, chunk_end} + end + end) + + new_start_block = last_written_block + 1 + {:ok, new_end_block} = Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + + delay = + if new_end_block == last_written_block do + # there is no new block, so wait for some time to let the chain issue the new block + max(block_check_interval - Timex.diff(Timex.now(), time_before, :milliseconds), 0) + else + 0 + end + + Process.send_after(self(), :continue, delay) + + {:noreply, %{state | start_block: new_start_block, end_block: new_end_block}} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + defp reorg_handle(reorg_block) do + {deleted_count, _} = + Repo.delete_all(from(b in Bridge, where: b.type == :deposit and b.block_number >= ^reorg_block)) + + if deleted_count > 0 do + Logger.warning( + "As L1 reorg was detected, some deposits with block_number >= #{reorg_block} were removed from polygon_zkevm_bridge table. Number of removed rows: #{deleted_count}." + ) + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l1_tokens.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l1_tokens.ex new file mode 100644 index 000000000000..034298174f78 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l1_tokens.ex @@ -0,0 +1,78 @@ +defmodule Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens do + @moduledoc """ + Fetches information about L1 tokens for zkEVM bridge. + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + import Ecto.Query + + alias Explorer.Repo + alias Indexer.{BufferedTask, Helper} + alias Indexer.Fetcher.PolygonZkevm.{Bridge, BridgeL1} + + @behaviour BufferedTask + + @default_max_batch_size 1 + @default_max_concurrency 10 + + @doc false + def child_spec([init_options, gen_server_options]) do + rpc = Application.get_all_env(:indexer)[BridgeL1][:rpc] + json_rpc_named_arguments = Helper.json_rpc_named_arguments(rpc) + + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.merge(state: json_rpc_named_arguments) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(_, _, _) do + {0, []} + end + + @impl BufferedTask + def run(l1_token_addresses, json_rpc_named_arguments) when is_list(l1_token_addresses) do + l1_token_addresses + |> Bridge.token_addresses_to_ids(json_rpc_named_arguments) + |> Enum.each(fn {l1_token_address, l1_token_id} -> + Repo.update_all( + from(b in Explorer.Chain.PolygonZkevm.Bridge, where: b.l1_token_address == ^l1_token_address), + set: [l1_token_id: l1_token_id, l1_token_address: nil] + ) + end) + end + + @doc """ + Fetches L1 token data asynchronously. + """ + def async_fetch(data) do + async_fetch(data, Application.get_env(:indexer, __MODULE__.Supervisor)[:enabled]) + end + + def async_fetch(_data, false), do: :ok + + def async_fetch(operations, _enabled) do + l1_token_addresses = + operations + |> Enum.reject(fn operation -> is_nil(operation.l1_token_address) end) + |> Enum.map(fn operation -> operation.l1_token_address end) + |> Enum.uniq() + + BufferedTask.buffer(__MODULE__, l1_token_addresses) + end + + defp defaults do + [ + flush_interval: 100, + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + poll: false, + task_supervisor: __MODULE__.TaskSupervisor + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l2.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l2.ex new file mode 100644 index 000000000000..2cd121881b2e --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge_l2.ex @@ -0,0 +1,176 @@ +defmodule Indexer.Fetcher.PolygonZkevm.BridgeL2 do + @moduledoc """ + Fills polygon_zkevm_bridge DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + import Explorer.Helper, only: [parse_integer: 1] + + import Indexer.Fetcher.PolygonZkevm.Bridge, + only: [get_logs_all: 3, import_operations: 1, prepare_operations: 3] + + alias Explorer.Chain.PolygonZkevm.{Bridge, Reader} + alias Explorer.Repo + alias Indexer.Helper + + @eth_get_logs_range_size 1000 + @fetcher_name :polygon_zkevm_bridge_l2 + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(args) do + json_rpc_named_arguments = args[:json_rpc_named_arguments] + {:ok, %{}, {:continue, json_rpc_named_arguments}} + end + + @impl GenServer + def handle_continue(json_rpc_named_arguments, _state) do + Logger.metadata(fetcher: @fetcher_name) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + Process.send_after(self(), :init_with_delay, 2000) + {:noreply, %{json_rpc_named_arguments: json_rpc_named_arguments}} + end + + @impl GenServer + def handle_info(:init_with_delay, %{json_rpc_named_arguments: json_rpc_named_arguments} = state) do + env = Application.get_all_env(:indexer)[__MODULE__] + + with {:start_block_undefined, false} <- {:start_block_undefined, is_nil(env[:start_block])}, + rpc_l1 = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonZkevm.BridgeL1][:rpc], + {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(rpc_l1)}, + {:bridge_contract_address_is_valid, true} <- + {:bridge_contract_address_is_valid, Helper.address_correct?(env[:bridge_contract])}, + start_block = parse_integer(env[:start_block]), + false <- is_nil(start_block), + true <- start_block > 0, + {last_l2_block_number, last_l2_transaction_hash} = Reader.last_l2_item(), + {:ok, latest_block} = Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000), + {:start_block_valid, true} <- + {:start_block_valid, + (start_block <= last_l2_block_number || last_l2_block_number == 0) && start_block <= latest_block}, + {:ok, last_l2_tx} <- Helper.get_transaction_by_hash(last_l2_transaction_hash, json_rpc_named_arguments), + {:l2_tx_not_found, false} <- {:l2_tx_not_found, !is_nil(last_l2_transaction_hash) && is_nil(last_l2_tx)} do + Process.send(self(), :continue, []) + + {:noreply, + %{ + bridge_contract: env[:bridge_contract], + json_rpc_named_arguments: json_rpc_named_arguments, + json_rpc_named_arguments_l1: Helper.json_rpc_named_arguments(rpc_l1), + end_block: latest_block, + start_block: max(start_block, last_l2_block_number) + }} + else + {:start_block_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, state} + + {:rpc_l1_undefined, true} -> + Logger.error("L1 RPC URL is not defined.") + {:stop, :normal, state} + + {:bridge_contract_address_is_valid, false} -> + Logger.error("PolygonZkEVMBridge contract address is invalid or not defined.") + {:stop, :normal, state} + + {:start_block_valid, false} -> + Logger.error("Invalid L2 Start Block value. Please, check the value and polygon_zkevm_bridge table.") + {:stop, :normal, state} + + {:error, error_data} -> + Logger.error( + "Cannot get last L2 transaction from RPC by its hash or latest block due to RPC error: #{inspect(error_data)}" + ) + + {:stop, :normal, state} + + {:l2_tx_not_found, true} -> + Logger.error( + "Cannot find last L2 transaction from RPC by its hash. Probably, there was a reorg on L2 chain. Please, check polygon_zkevm_bridge table." + ) + + {:stop, :normal, state} + + _ -> + Logger.error("L2 Start Block is invalid or zero.") + {:stop, :normal, state} + end + end + + @impl GenServer + def handle_info( + :continue, + %{ + bridge_contract: bridge_contract, + start_block: start_block, + end_block: end_block, + json_rpc_named_arguments: json_rpc_named_arguments, + json_rpc_named_arguments_l1: json_rpc_named_arguments_l1 + } = state + ) do + start_block..end_block + |> Enum.chunk_every(@eth_get_logs_range_size) + |> Enum.each(fn current_chunk -> + chunk_start = List.first(current_chunk) + chunk_end = List.last(current_chunk) + + if chunk_start <= chunk_end do + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L2) + + operations = + {chunk_start, chunk_end} + |> get_logs_all(bridge_contract, json_rpc_named_arguments) + |> prepare_operations(json_rpc_named_arguments, json_rpc_named_arguments_l1) + + import_operations(operations) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(operations)} L2 operation(s)", + :L2 + ) + end + end) + + {:stop, :normal, state} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + def reorg_handle(reorg_block) do + {deleted_count, _} = + Repo.delete_all(from(b in Bridge, where: b.type == :withdrawal and b.block_number >= ^reorg_block)) + + if deleted_count > 0 do + Logger.warning( + "As L2 reorg was detected, some withdrawals with block_number >= #{reorg_block} were removed from polygon_zkevm_bridge table. Number of removed rows: #{deleted_count}." + ) + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zkevm/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex similarity index 84% rename from apps/indexer/lib/indexer/fetcher/zkevm/transaction_batch.ex rename to apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex index d59da1203e63..834fca842b9b 100644 --- a/apps/indexer/lib/indexer/fetcher/zkevm/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex @@ -1,6 +1,6 @@ -defmodule Indexer.Fetcher.Zkevm.TransactionBatch do +defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do @moduledoc """ - Fills zkevm_transaction_batches DB table. + Fills polygon_zkevm_transaction_batches DB table. """ use GenServer @@ -12,7 +12,8 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do alias Explorer.Chain alias Explorer.Chain.Events.Publisher - alias Explorer.Chain.Zkevm.Reader + alias Explorer.Chain.PolygonZkevm.Reader + alias Indexer.Helper @zero_hash "0000000000000000000000000000000000000000000000000000000000000000" @@ -33,9 +34,9 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do @impl GenServer def init(args) do - Logger.metadata(fetcher: :zkevm_transaction_batches) + Logger.metadata(fetcher: :polygon_zkevm_transaction_batches) - config = Application.get_all_env(:indexer)[Indexer.Fetcher.Zkevm.TransactionBatch] + config = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonZkevm.TransactionBatch] chunk_size = config[:chunk_size] recheck_interval = config[:recheck_interval] @@ -154,6 +155,7 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do end defp fetch_and_save_batches(batch_start, batch_end, json_rpc_named_arguments) do + # For every batch from batch_start to batch_end request the batch info requests = batch_start |> Range.new(batch_end, 1) @@ -168,8 +170,9 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do error_message = &"Cannot call zkevm_getBatchByNumber for the batch range #{batch_start}..#{batch_end}. Error: #{inspect(&1)}" - {:ok, responses} = repeated_call(&json_rpc/2, [requests, json_rpc_named_arguments], error_message, 3) + {:ok, responses} = Helper.repeated_call(&json_rpc/2, [requests, json_rpc_named_arguments], error_message, 3) + # For every batch info extract batches' L1 sequence tx and L1 verify tx {sequence_hashes, verify_hashes} = responses |> Enum.reduce({[], []}, fn res, {sequences, verifies} = _acc -> @@ -193,8 +196,10 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do {sequences, verifies} end) + # All L1 transactions in one list without repetition l1_tx_hashes = Enum.uniq(sequence_hashes ++ verify_hashes) + # Receive all IDs for L1 txs hash_to_id = l1_tx_hashes |> Reader.lifecycle_transactions() @@ -202,28 +207,39 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do Map.put(acc, hash.bytes, id) end) + # For every batch build batch representation, collect associated L1 and L2 transactions {batches_to_import, l2_txs_to_import, l1_txs_to_import, _, _} = responses |> Enum.reduce({[], [], [], Reader.next_id(), hash_to_id}, fn res, {batches, l2_txs, l1_txs, next_id, hash_to_id} = _acc -> number = quantity_to_integer(Map.get(res.result, "number")) - {:ok, timestamp} = DateTime.from_unix(quantity_to_integer(Map.get(res.result, "timestamp"))) + + # the timestamp is undefined for unfinalized batches + timestamp = + case DateTime.from_unix(quantity_to_integer(Map.get(res.result, "timestamp", 0xFFFFFFFFFFFFFFFF))) do + {:ok, ts} -> ts + _ -> nil + end + l2_transaction_hashes = Map.get(res.result, "transactions") global_exit_root = Map.get(res.result, "globalExitRoot") acc_input_hash = Map.get(res.result, "accInputHash") state_root = Map.get(res.result, "stateRoot") + # Get ID for sequence transaction (new ID if the batch is just sequenced) {sequence_id, l1_txs, next_id, hash_to_id} = res.result |> get_tx_hash("sendSequencesTxHash") |> handle_tx_hash(hash_to_id, next_id, l1_txs, false) + # Get ID for verify transaction (new ID if the batch is just verified) {verify_id, l1_txs, next_id, hash_to_id} = res.result |> get_tx_hash("verifyBatchTxHash") |> handle_tx_hash(hash_to_id, next_id, l1_txs, true) + # Associate every transaction from batch with the batch number l2_txs_append = l2_transaction_hashes |> Kernel.||([]) @@ -248,17 +264,19 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do {[batch | batches], l2_txs ++ l2_txs_append, l1_txs, next_id, hash_to_id} end) + # Update batches list, L1 transactions list and L2 transaction list {:ok, _} = Chain.import(%{ - zkevm_lifecycle_transactions: %{params: l1_txs_to_import}, - zkevm_transaction_batches: %{params: batches_to_import}, - zkevm_batch_transactions: %{params: l2_txs_to_import}, + polygon_zkevm_lifecycle_transactions: %{params: l1_txs_to_import}, + polygon_zkevm_transaction_batches: %{params: batches_to_import}, + polygon_zkevm_batch_transactions: %{params: l2_txs_to_import}, timeout: :infinity }) confirmed_batches = Enum.filter(batches_to_import, fn batch -> not is_nil(batch.sequence_id) and batch.sequence_id > 0 end) + # Publish update for open batches Views in BS app with the new confirmed batches if not Enum.empty?(confirmed_batches) do Publisher.broadcast([{:zkevm_confirmed_batches, confirmed_batches}], :realtime) end @@ -273,7 +291,7 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do error_message = &"Cannot call zkevm_batchNumber. Error: #{inspect(&1)}" - {:ok, responses} = repeated_call(&json_rpc/2, [requests, json_rpc_named_arguments], error_message, 3) + {:ok, responses} = Helper.repeated_call(&json_rpc/2, [requests, json_rpc_named_arguments], error_message, 3) latest_batch_number = Enum.find_value(responses, fn resp -> if resp.id == 0, do: quantity_to_integer(resp.result) end) @@ -310,23 +328,4 @@ defmodule Indexer.Fetcher.Zkevm.TransactionBatch do {nil, l1_txs, next_id, hash_to_id} end end - - defp repeated_call(func, args, error_message, retries_left) do - case apply(func, args) do - {:ok, _} = res -> - res - - {:error, message} = err -> - retries_left = retries_left - 1 - - if retries_left <= 0 do - Logger.error(error_message.(message)) - err - else - Logger.error("#{error_message.(message)} Retrying...") - :timer.sleep(3000) - repeated_call(func, args, error_message, retries_left) - end - end - end end diff --git a/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex b/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex new file mode 100644 index 000000000000..5da08bf1d717 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex @@ -0,0 +1,169 @@ +defmodule Indexer.Fetcher.RollupL1ReorgMonitor do + @moduledoc """ + A module to catch L1 reorgs and notify a rollup module about it. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + alias Indexer.{BoundQueue, Helper} + + @fetcher_name :rollup_l1_reorg_monitor + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + Logger.metadata(fetcher: @fetcher_name) + + modules_can_use_reorg_monitor = [ + Indexer.Fetcher.Optimism.OutputRoot, + Indexer.Fetcher.Optimism.TxnBatch, + Indexer.Fetcher.Optimism.WithdrawalEvent, + Indexer.Fetcher.PolygonEdge.Deposit, + Indexer.Fetcher.PolygonEdge.WithdrawalExit, + Indexer.Fetcher.PolygonZkevm.BridgeL1, + Indexer.Fetcher.Shibarium.L1 + ] + + modules_using_reorg_monitor = + modules_can_use_reorg_monitor + |> Enum.reject(fn module -> + module_config = Application.get_all_env(:indexer)[module] + is_nil(module_config[:start_block]) and is_nil(module_config[:start_block_l1]) + end) + + if Enum.empty?(modules_using_reorg_monitor) do + # don't start reorg monitor as there is no module which would use it + :ignore + else + # As there cannot be different modules for different rollups at the same time, + # it's correct to only get the first item of the list. + # For example, Indexer.Fetcher.PolygonEdge.Deposit and Indexer.Fetcher.PolygonEdge.WithdrawalExit can be in the list + # because they are for the same rollup, but Indexer.Fetcher.Shibarium.L1 and Indexer.Fetcher.PolygonZkevm.BridgeL1 cannot (as they are for different rollups). + module_using_reorg_monitor = Enum.at(modules_using_reorg_monitor, 0) + + l1_rpc = + cond do + Enum.member?( + [Indexer.Fetcher.PolygonEdge.Deposit, Indexer.Fetcher.PolygonEdge.WithdrawalExit], + module_using_reorg_monitor + ) -> + # there can be more than one PolygonEdge.* modules, so we get the common L1 RPC URL for them from Indexer.Fetcher.PolygonEdge + Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonEdge][:polygon_edge_l1_rpc] + + Enum.member?( + [ + Indexer.Fetcher.Optimism.OutputRoot, + Indexer.Fetcher.Optimism.TxnBatch, + Indexer.Fetcher.Optimism.WithdrawalEvent + ], + module_using_reorg_monitor + ) -> + # there can be more than one Optimism.* modules, so we get the common L1 RPC URL for them from Indexer.Fetcher.Optimism + Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism][:optimism_l1_rpc] + + true -> + Application.get_all_env(:indexer)[module_using_reorg_monitor][:rpc] + end + + json_rpc_named_arguments = Helper.json_rpc_named_arguments(l1_rpc) + + {:ok, block_check_interval, _} = Helper.get_block_check_interval(json_rpc_named_arguments) + + Process.send(self(), :reorg_monitor, []) + + {:ok, + %{ + block_check_interval: block_check_interval, + json_rpc_named_arguments: json_rpc_named_arguments, + modules: modules_using_reorg_monitor, + prev_latest: 0 + }} + end + end + + @impl GenServer + def handle_info( + :reorg_monitor, + %{ + block_check_interval: block_check_interval, + json_rpc_named_arguments: json_rpc_named_arguments, + modules: modules, + prev_latest: prev_latest + } = state + ) do + {:ok, latest} = Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + + if latest < prev_latest do + Logger.warning("Reorg detected: previous latest block ##{prev_latest}, current latest block ##{latest}.") + Enum.each(modules, &reorg_block_push(latest, &1)) + end + + Process.send_after(self(), :reorg_monitor, block_check_interval) + + {:noreply, %{state | prev_latest: latest}} + end + + @doc """ + Pops the number of reorg block from the front of the queue for the specified rollup module. + Returns `nil` if the reorg queue is empty. + """ + @spec reorg_block_pop(module()) :: non_neg_integer() | nil + def reorg_block_pop(module) do + table_name = reorg_table_name(module) + + case BoundQueue.pop_front(reorg_queue_get(table_name)) do + {:ok, {block_number, updated_queue}} -> + :ets.insert(table_name, {:queue, updated_queue}) + block_number + + {:error, :empty} -> + nil + end + end + + defp reorg_block_push(block_number, module) do + table_name = reorg_table_name(module) + {:ok, updated_queue} = BoundQueue.push_back(reorg_queue_get(table_name), block_number) + :ets.insert(table_name, {:queue, updated_queue}) + end + + defp reorg_queue_get(table_name) do + if :ets.whereis(table_name) == :undefined do + :ets.new(table_name, [ + :set, + :named_table, + :public, + read_concurrency: true, + write_concurrency: true + ]) + end + + with info when info != :undefined <- :ets.info(table_name), + [{_, value}] <- :ets.lookup(table_name, :queue) do + value + else + _ -> %BoundQueue{} + end + end + + defp reorg_table_name(module) do + :"#{module}#{:_reorgs}" + end +end diff --git a/apps/indexer/lib/indexer/fetcher/rootstock_data.ex b/apps/indexer/lib/indexer/fetcher/rootstock_data.ex new file mode 100644 index 000000000000..09b683e6563a --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/rootstock_data.ex @@ -0,0 +1,170 @@ +defmodule Indexer.Fetcher.RootstockData do + @moduledoc """ + Refetch `minimum_gas_price`, `bitcoin_merged_mining_header`, `bitcoin_merged_mining_coinbase_transaction`, + `bitcoin_merged_mining_merkle_proof`, `hash_for_merged_mining` fields for blocks that were indexed before app update. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + alias EthereumJSONRPC.Blocks + alias Explorer.Chain.Block + alias Explorer.Repo + + @interval :timer.seconds(3) + @batch_size 10 + @concurrency 5 + @db_batch_size 300 + + defstruct blocks_to_fetch: [], + interval: @interval, + json_rpc_named_arguments: [], + batch_size: @batch_size, + max_concurrency: @concurrency, + db_batch_size: @db_batch_size + + def child_spec([init_arguments]) do + child_spec([init_arguments, []]) + end + + def child_spec([_init_arguments, _gen_server_options] = start_link_arguments) do + default = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments} + } + + Supervisor.child_spec(default, restart: :transient) + end + + def start_link(arguments, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, arguments, gen_server_options) + end + + @impl GenServer + def init(opts) when is_list(opts) do + Logger.metadata(fetcher: :rootstock_data) + + json_rpc_named_arguments = opts[:json_rpc_named_arguments] + + unless json_rpc_named_arguments do + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.init to allow for json_rpc calls when running." + end + + state = %__MODULE__{ + blocks_to_fetch: nil, + interval: opts[:interval] || Application.get_env(:indexer, __MODULE__)[:interval], + json_rpc_named_arguments: json_rpc_named_arguments, + batch_size: opts[:batch_size] || Application.get_env(:indexer, __MODULE__)[:batch_size], + max_concurrency: opts[:max_concurrency] || Application.get_env(:indexer, __MODULE__)[:max_concurrency], + db_batch_size: opts[:db_batch_size] || Application.get_env(:indexer, __MODULE__)[:db_batch_size] + } + + Process.send_after(self(), :fetch_rootstock_data, state.interval) + + {:ok, state, {:continue, :fetch_blocks}} + end + + @impl GenServer + def handle_continue(:fetch_blocks, state), do: fetch_blocks(state) + + @impl GenServer + def handle_info(:fetch_blocks, state), do: fetch_blocks(state) + + @impl GenServer + def handle_info( + :fetch_rootstock_data, + %__MODULE__{ + blocks_to_fetch: blocks_to_fetch, + interval: interval, + json_rpc_named_arguments: json_rpc_named_arguments, + batch_size: batch_size, + max_concurrency: concurrency + } = state + ) do + if Enum.empty?(blocks_to_fetch) do + send(self(), :fetch_blocks) + {:noreply, state} + else + new_blocks_to_fetch = + blocks_to_fetch + |> Stream.chunk_every(batch_size) + |> Task.async_stream( + &{EthereumJSONRPC.fetch_blocks_by_numbers( + Enum.map(&1, fn b -> b.number end), + json_rpc_named_arguments, + false + ), &1}, + max_concurrency: concurrency, + timeout: :infinity, + zip_input_on_exit: true + ) + |> Enum.reduce([], &fetch_reducer/2) + + Process.send_after(self(), :fetch_rootstock_data, interval) + + {:noreply, %__MODULE__{state | blocks_to_fetch: new_blocks_to_fetch}} + end + end + + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + def handle_info( + {:DOWN, _ref, :process, _pid, reason}, + state + ) do + if reason === :normal do + {:noreply, state} + else + Logger.error(fn -> "Rootstock data fetcher task exited due to #{inspect(reason)}." end) + {:noreply, state} + end + end + + defp fetch_blocks(%__MODULE__{db_batch_size: db_batch_size, interval: interval} = state) do + blocks_to_fetch = db_batch_size |> Block.blocks_without_rootstock_data_query() |> Repo.all() + + if Enum.empty?(blocks_to_fetch) do + Logger.info("Rootstock data from old blocks are fetched.") + + {:stop, :normal, state} + else + [%Block{number: max_number} | _] = blocks_to_fetch + + Logger.info( + "Rootstock data will now be fetched for #{Enum.count(blocks_to_fetch)} blocks starting from #{max_number}." + ) + + Process.send_after(self(), :fetch_rootstock_data, interval) + + {:noreply, %__MODULE__{state | blocks_to_fetch: blocks_to_fetch}} + end + end + + defp fetch_reducer({:ok, {{:ok, %Blocks{blocks_params: block_params}}, blocks}}, acc) do + blocks_map = Map.new(blocks, fn b -> {b.number, b} end) + + for block_param <- block_params, + block = blocks_map[block_param.number], + block_param.hash == to_string(block.hash) do + block |> Block.changeset(block_param) |> Repo.update() + end + + acc + end + + defp fetch_reducer({:ok, {{:error, reason}, blocks}}, acc) do + Logger.error("failed to fetch: " <> inspect(reason) <> ". Retrying.") + [blocks | acc] |> List.flatten() + end + + defp fetch_reducer({:exit, {blocks, reason}}, acc) do + Logger.error("failed to fetch: " <> inspect(reason) <> ". Retrying.") + [blocks | acc] |> List.flatten() + end +end diff --git a/apps/indexer/lib/indexer/fetcher/shibarium/helper.ex b/apps/indexer/lib/indexer/fetcher/shibarium/helper.ex new file mode 100644 index 000000000000..6dceb3eedb78 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/shibarium/helper.ex @@ -0,0 +1,148 @@ +defmodule Indexer.Fetcher.Shibarium.Helper do + @moduledoc """ + Common functions for Indexer.Fetcher.Shibarium.* modules. + """ + + import Ecto.Query + + alias Explorer.Chain.Cache.ShibariumCounter + alias Explorer.Chain.Shibarium.{Bridge, Reader} + alias Explorer.Repo + + @empty_hash "0x0000000000000000000000000000000000000000000000000000000000000000" + + @doc """ + Calculates Shibarium Bridge operation hash as hash_256(user_address, amount_or_id, erc1155_ids, erc1155_amounts, operation_id). + """ + @spec calc_operation_hash(binary(), non_neg_integer() | nil, list(), list(), non_neg_integer()) :: binary() + def calc_operation_hash(user, amount_or_id, erc1155_ids, erc1155_amounts, operation_id) do + user_binary = + user + |> String.trim_leading("0x") + |> Base.decode16!(case: :mixed) + + amount_or_id = + if is_nil(amount_or_id) and not Enum.empty?(erc1155_ids) do + 0 + else + amount_or_id + end + + operation_encoded = + ABI.encode("(address,uint256,uint256[],uint256[],uint256)", [ + { + user_binary, + amount_or_id, + erc1155_ids, + erc1155_amounts, + operation_id + } + ]) + + "0x" <> + (operation_encoded + |> ExKeccak.hash_256() + |> Base.encode16(case: :lower)) + end + + @doc """ + Prepares a list of Shibarium Bridge operations to import them into database. + Tries to bind the given operations to the existing ones in DB first. + If they don't exist, prepares the insertion list and returns it. + """ + @spec prepare_insert_items(list(), module()) :: list() + def prepare_insert_items(operations, calling_module) do + operations + |> Enum.reduce([], fn op, acc -> + if bind_existing_operation_in_db(op, calling_module) == 0 do + [op | acc] + else + acc + end + end) + |> Enum.reverse() + |> Enum.reduce(%{}, fn item, acc -> + Map.put(acc, {item.operation_hash, item.l1_transaction_hash, item.l2_transaction_hash}, item) + end) + |> Map.values() + end + + @doc """ + Recalculate the cached count of complete rows for deposits and withdrawals. + """ + @spec recalculate_cached_count() :: no_return() + def recalculate_cached_count do + ShibariumCounter.deposits_count_save(Reader.deposits_count()) + ShibariumCounter.withdrawals_count_save(Reader.withdrawals_count()) + end + + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + defp bind_existing_operation_in_db(op, calling_module) do + {query, set} = make_query_for_bind(op, calling_module) + + updated_count = + try do + {updated_count, _} = + Repo.update_all( + from(b in Bridge, + join: s in subquery(query), + on: + b.operation_hash == s.operation_hash and b.l1_transaction_hash == s.l1_transaction_hash and + b.l2_transaction_hash == s.l2_transaction_hash + ), + set: set + ) + + updated_count + rescue + error in Postgrex.Error -> + # if this is unique violation, we just ignore such an operation as it was inserted before + if error.postgres.code != :unique_violation do + reraise error, __STACKTRACE__ + end + end + + # increment the cached count of complete rows + case !is_nil(updated_count) && updated_count > 0 && op.operation_type do + :deposit -> ShibariumCounter.deposits_count_save(updated_count, true) + :withdrawal -> ShibariumCounter.withdrawals_count_save(updated_count, true) + false -> nil + end + + updated_count + end + + defp make_query_for_bind(op, calling_module) when calling_module == Indexer.Fetcher.Shibarium.L1 do + query = + from(sb in Bridge, + where: + sb.operation_hash == ^op.operation_hash and sb.operation_type == ^op.operation_type and + sb.l2_transaction_hash != ^@empty_hash and sb.l1_transaction_hash == ^@empty_hash, + order_by: [asc: sb.l2_block_number], + limit: 1 + ) + + set = + [l1_transaction_hash: op.l1_transaction_hash, l1_block_number: op.l1_block_number] ++ + if(op.operation_type == :deposit, do: [timestamp: op.timestamp], else: []) + + {query, set} + end + + defp make_query_for_bind(op, calling_module) when calling_module == Indexer.Fetcher.Shibarium.L2 do + query = + from(sb in Bridge, + where: + sb.operation_hash == ^op.operation_hash and sb.operation_type == ^op.operation_type and + sb.l1_transaction_hash != ^@empty_hash and sb.l2_transaction_hash == ^@empty_hash, + order_by: [asc: sb.l1_block_number], + limit: 1 + ) + + set = + [l2_transaction_hash: op.l2_transaction_hash, l2_block_number: op.l2_block_number] ++ + if(op.operation_type == :withdrawal, do: [timestamp: op.timestamp], else: []) + + {query, set} + end +end diff --git a/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex b/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex new file mode 100644 index 000000000000..4a3889dd2a4d --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex @@ -0,0 +1,656 @@ +defmodule Indexer.Fetcher.Shibarium.L1 do + @moduledoc """ + Fills shibarium_bridge DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, + only: [ + integer_to_quantity: 1, + json_rpc: 2, + quantity_to_integer: 1, + request: 1 + ] + + import Explorer.Helper, only: [parse_integer: 1, decode_data: 2] + + import Indexer.Fetcher.Shibarium.Helper, + only: [calc_operation_hash: 5, prepare_insert_items: 2, recalculate_cached_count: 0] + + alias Explorer.Chain.Shibarium.Bridge + alias Explorer.{Chain, Repo} + alias Indexer.Fetcher.RollupL1ReorgMonitor + alias Indexer.Helper + alias Indexer.Transform.Addresses + + @block_check_interval_range_size 100 + @eth_get_logs_range_size 1000 + @fetcher_name :shibarium_bridge_l1 + @empty_hash "0x0000000000000000000000000000000000000000000000000000000000000000" + + # 32-byte signature of the event NewDepositBlock(address indexed owner, address indexed token, uint256 amountOrNFTId, uint256 depositBlockId) + @new_deposit_block_event "0x1dadc8d0683c6f9824e885935c1bec6f76816730dcec148dda8cf25a7b9f797b" + + # 32-byte signature of the event LockedEther(address indexed depositor, address indexed depositReceiver, uint256 amount) + @locked_ether_event "0x3e799b2d61372379e767ef8f04d65089179b7a6f63f9be3065806456c7309f1b" + + # 32-byte signature of the event LockedERC20(address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256 amount) + @locked_erc20_event "0x9b217a401a5ddf7c4d474074aff9958a18d48690d77cc2151c4706aa7348b401" + + # 32-byte signature of the event LockedERC721(address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256 tokenId) + @locked_erc721_event "0x8357472e13612a8c3d6f3e9d71fbba8a78ab77dbdcc7fcf3b7b645585f0bbbfc" + + # 32-byte signature of the event LockedERC721Batch(address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256[] tokenIds) + @locked_erc721_batch_event "0x5345c2beb0e49c805f42bb70c4ec5c3c3d9680ce45b8f4529c028d5f3e0f7a0d" + + # 32-byte signature of the event LockedBatchERC1155(address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256[] ids, uint256[] amounts) + @locked_batch_erc1155_event "0x5a921678b5779e4471b77219741a417a6ad6ec5d89fa5c8ce8cd7bd2d9f34186" + + # 32-byte signature of the event Withdraw(uint256 indexed exitId, address indexed user, address indexed token, uint256 amount) + @withdraw_event "0xfeb2000dca3e617cd6f3a8bbb63014bb54a124aac6ccbf73ee7229b4cd01f120" + + # 32-byte signature of the event ExitedEther(address indexed exitor, uint256 amount) + @exited_ether_event "0x0fc0eed41f72d3da77d0f53b9594fc7073acd15ee9d7c536819a70a67c57ef3c" + + # 32-byte signature of the event Transfer(address indexed from, address indexed to, uint256 value) + @transfer_event "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + # 32-byte signature of the event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) + @transfer_single_event "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" + + # 32-byte signature of the event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) + @transfer_batch_event "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb" + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, :ok}} + end + + @impl GenServer + def handle_continue(_, state) do + Logger.metadata(fetcher: @fetcher_name) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + Process.send_after(self(), :wait_for_l2, 2000) + {:noreply, state} + end + + @impl GenServer + def handle_info(:wait_for_l2, state) do + if is_nil(Process.whereis(Indexer.Fetcher.Shibarium.L2)) do + Process.send(self(), :init_with_delay, []) + else + Process.send_after(self(), :wait_for_l2, 2000) + end + + {:noreply, state} + end + + @impl GenServer + def handle_info(:init_with_delay, _state) do + env = Application.get_all_env(:indexer)[__MODULE__] + + with {:start_block_undefined, false} <- {:start_block_undefined, is_nil(env[:start_block])}, + {:reorg_monitor_started, true} <- {:reorg_monitor_started, !is_nil(Process.whereis(RollupL1ReorgMonitor))}, + rpc = env[:rpc], + {:rpc_undefined, false} <- {:rpc_undefined, is_nil(rpc)}, + {:deposit_manager_address_is_valid, true} <- + {:deposit_manager_address_is_valid, Helper.address_correct?(env[:deposit_manager_proxy])}, + {:ether_predicate_address_is_valid, true} <- + {:ether_predicate_address_is_valid, Helper.address_correct?(env[:ether_predicate_proxy])}, + {:erc20_predicate_address_is_valid, true} <- + {:erc20_predicate_address_is_valid, Helper.address_correct?(env[:erc20_predicate_proxy])}, + {:erc721_predicate_address_is_valid, true} <- + {:erc721_predicate_address_is_valid, + is_nil(env[:erc721_predicate_proxy]) or Helper.address_correct?(env[:erc721_predicate_proxy])}, + {:erc1155_predicate_address_is_valid, true} <- + {:erc1155_predicate_address_is_valid, + is_nil(env[:erc1155_predicate_proxy]) or Helper.address_correct?(env[:erc1155_predicate_proxy])}, + {:withdraw_manager_address_is_valid, true} <- + {:withdraw_manager_address_is_valid, Helper.address_correct?(env[:withdraw_manager_proxy])}, + start_block = parse_integer(env[:start_block]), + false <- is_nil(start_block), + true <- start_block > 0, + {last_l1_block_number, last_l1_transaction_hash} <- get_last_l1_item(), + {:start_block_valid, true} <- + {:start_block_valid, start_block <= last_l1_block_number || last_l1_block_number == 0}, + json_rpc_named_arguments = json_rpc_named_arguments(rpc), + {:ok, last_l1_tx} <- Helper.get_transaction_by_hash(last_l1_transaction_hash, json_rpc_named_arguments), + {:l1_tx_not_found, false} <- {:l1_tx_not_found, !is_nil(last_l1_transaction_hash) && is_nil(last_l1_tx)}, + {:ok, block_check_interval, latest_block} <- get_block_check_interval(json_rpc_named_arguments), + {:start_block_valid, true} <- {:start_block_valid, start_block <= latest_block} do + recalculate_cached_count() + + Process.send(self(), :continue, []) + + {:noreply, + %{ + deposit_manager_proxy: env[:deposit_manager_proxy], + ether_predicate_proxy: env[:ether_predicate_proxy], + erc20_predicate_proxy: env[:erc20_predicate_proxy], + erc721_predicate_proxy: env[:erc721_predicate_proxy], + erc1155_predicate_proxy: env[:erc1155_predicate_proxy], + withdraw_manager_proxy: env[:withdraw_manager_proxy], + block_check_interval: block_check_interval, + start_block: max(start_block, last_l1_block_number), + end_block: latest_block, + json_rpc_named_arguments: json_rpc_named_arguments + }} + else + {:start_block_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, %{}} + + {:reorg_monitor_started, false} -> + Logger.error("Cannot start this process as Indexer.Fetcher.RollupL1ReorgMonitor is not started.") + {:stop, :normal, %{}} + + {:rpc_undefined, true} -> + Logger.error("L1 RPC URL is not defined.") + {:stop, :normal, %{}} + + {:deposit_manager_address_is_valid, false} -> + Logger.error("DepositManagerProxy contract address is invalid or not defined.") + {:stop, :normal, %{}} + + {:ether_predicate_address_is_valid, false} -> + Logger.error("EtherPredicateProxy contract address is invalid or not defined.") + {:stop, :normal, %{}} + + {:erc20_predicate_address_is_valid, false} -> + Logger.error("ERC20PredicateProxy contract address is invalid or not defined.") + {:stop, :normal, %{}} + + {:erc721_predicate_address_is_valid, false} -> + Logger.error("ERC721PredicateProxy contract address is invalid.") + {:stop, :normal, %{}} + + {:erc1155_predicate_address_is_valid, false} -> + Logger.error("ERC1155PredicateProxy contract address is invalid.") + {:stop, :normal, %{}} + + {:withdraw_manager_address_is_valid, false} -> + Logger.error("WithdrawManagerProxy contract address is invalid or not defined.") + {:stop, :normal, %{}} + + {:start_block_valid, false} -> + Logger.error("Invalid L1 Start Block value. Please, check the value and shibarium_bridge table.") + {:stop, :normal, %{}} + + {:error, error_data} -> + Logger.error( + "Cannot get last L1 transaction from RPC by its hash, latest block, or block timestamp by its number due to RPC error: #{inspect(error_data)}" + ) + + {:stop, :normal, %{}} + + {:l1_tx_not_found, true} -> + Logger.error( + "Cannot find last L1 transaction from RPC by its hash. Probably, there was a reorg on L1 chain. Please, check shibarium_bridge table." + ) + + {:stop, :normal, %{}} + + _ -> + Logger.error("L1 Start Block is invalid or zero.") + {:stop, :normal, %{}} + end + end + + @impl GenServer + def handle_info( + :continue, + %{ + deposit_manager_proxy: deposit_manager_proxy, + ether_predicate_proxy: ether_predicate_proxy, + erc20_predicate_proxy: erc20_predicate_proxy, + erc721_predicate_proxy: erc721_predicate_proxy, + erc1155_predicate_proxy: erc1155_predicate_proxy, + withdraw_manager_proxy: withdraw_manager_proxy, + block_check_interval: block_check_interval, + start_block: start_block, + end_block: end_block, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + time_before = Timex.now() + + last_written_block = + start_block..end_block + |> Enum.chunk_every(@eth_get_logs_range_size) + |> Enum.reduce_while(start_block - 1, fn current_chunk, _ -> + chunk_start = List.first(current_chunk) + chunk_end = List.last(current_chunk) + + if chunk_start <= chunk_end do + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) + + operations = + {chunk_start, chunk_end} + |> get_logs_all( + deposit_manager_proxy, + ether_predicate_proxy, + erc20_predicate_proxy, + erc721_predicate_proxy, + erc1155_predicate_proxy, + withdraw_manager_proxy, + json_rpc_named_arguments + ) + |> prepare_operations(json_rpc_named_arguments) + + insert_items = prepare_insert_items(operations, __MODULE__) + + addresses = + Addresses.extract_addresses(%{ + shibarium_bridge_operations: insert_items + }) + + {:ok, _} = + Chain.import(%{ + addresses: %{params: addresses, on_conflict: :nothing}, + shibarium_bridge_operations: %{params: insert_items}, + timeout: :infinity + }) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(operations)} L1 operation(s)", + :L1 + ) + end + + reorg_block = RollupL1ReorgMonitor.reorg_block_pop(__MODULE__) + + if !is_nil(reorg_block) && reorg_block > 0 do + reorg_handle(reorg_block) + {:halt, if(reorg_block <= chunk_end, do: reorg_block - 1, else: chunk_end)} + else + {:cont, chunk_end} + end + end) + + new_start_block = last_written_block + 1 + + {:ok, new_end_block} = + Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) + + delay = + if new_end_block == last_written_block do + # there is no new block, so wait for some time to let the chain issue the new block + max(block_check_interval - Timex.diff(Timex.now(), time_before, :milliseconds), 0) + else + 0 + end + + Process.send_after(self(), :continue, delay) + + {:noreply, %{state | start_block: new_start_block, end_block: new_end_block}} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + defp filter_deposit_events(events) do + Enum.filter(events, fn event -> + topic0 = Enum.at(event["topics"], 0) + deposit?(topic0) + end) + end + + defp get_block_check_interval(json_rpc_named_arguments) do + with {:ok, latest_block} <- Helper.get_block_number_by_tag("latest", json_rpc_named_arguments), + first_block = max(latest_block - @block_check_interval_range_size, 1), + {:ok, first_block_timestamp} <- Helper.get_block_timestamp_by_number(first_block, json_rpc_named_arguments), + {:ok, last_safe_block_timestamp} <- + Helper.get_block_timestamp_by_number(latest_block, json_rpc_named_arguments) do + block_check_interval = + ceil((last_safe_block_timestamp - first_block_timestamp) / (latest_block - first_block) * 1000 / 2) + + Logger.info("Block check interval is calculated as #{block_check_interval} ms.") + {:ok, block_check_interval, latest_block} + else + {:error, error} -> + {:error, "Failed to calculate block check interval due to #{inspect(error)}"} + end + end + + defp get_last_l1_item do + query = + from(sb in Bridge, + select: {sb.l1_block_number, sb.l1_transaction_hash}, + where: not is_nil(sb.l1_block_number), + order_by: [desc: sb.l1_block_number], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end + + defp get_logs(from_block, to_block, address, topics, json_rpc_named_arguments, retries) do + processed_from_block = integer_to_quantity(from_block) + processed_to_block = integer_to_quantity(to_block) + + req = + request(%{ + id: 0, + method: "eth_getLogs", + params: [ + %{ + :fromBlock => processed_from_block, + :toBlock => processed_to_block, + :address => address, + :topics => topics + } + ] + }) + + error_message = &"Cannot fetch logs for the block range #{from_block}..#{to_block}. Error: #{inspect(&1)}" + + Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + end + + defp get_logs_all( + {chunk_start, chunk_end}, + deposit_manager_proxy, + ether_predicate_proxy, + erc20_predicate_proxy, + erc721_predicate_proxy, + erc1155_predicate_proxy, + withdraw_manager_proxy, + json_rpc_named_arguments + ) do + {:ok, known_tokens_result} = + get_logs( + chunk_start, + chunk_end, + [deposit_manager_proxy, ether_predicate_proxy, erc20_predicate_proxy, withdraw_manager_proxy], + [ + [ + @new_deposit_block_event, + @locked_ether_event, + @locked_erc20_event, + @locked_erc721_event, + @locked_erc721_batch_event, + @locked_batch_erc1155_event, + @withdraw_event, + @exited_ether_event + ] + ], + json_rpc_named_arguments, + Helper.infinite_retries_number() + ) + + contract_addresses = + if is_nil(erc721_predicate_proxy) do + [pad_address_hash(erc20_predicate_proxy)] + else + [pad_address_hash(erc20_predicate_proxy), pad_address_hash(erc721_predicate_proxy)] + end + + {:ok, unknown_erc20_erc721_tokens_result} = + get_logs( + chunk_start, + chunk_end, + nil, + [ + @transfer_event, + contract_addresses + ], + json_rpc_named_arguments, + Helper.infinite_retries_number() + ) + + {:ok, unknown_erc1155_tokens_result} = + if is_nil(erc1155_predicate_proxy) do + {:ok, []} + else + get_logs( + chunk_start, + chunk_end, + nil, + [ + [@transfer_single_event, @transfer_batch_event], + nil, + pad_address_hash(erc1155_predicate_proxy) + ], + json_rpc_named_arguments, + Helper.infinite_retries_number() + ) + end + + known_tokens_result ++ unknown_erc20_erc721_tokens_result ++ unknown_erc1155_tokens_result + end + + defp get_op_user(topic0, event) do + cond do + Enum.member?([@new_deposit_block_event, @exited_ether_event], topic0) -> + truncate_address_hash(Enum.at(event["topics"], 1)) + + Enum.member?( + [ + @locked_ether_event, + @locked_erc20_event, + @locked_erc721_event, + @locked_erc721_batch_event, + @locked_batch_erc1155_event, + @withdraw_event, + @transfer_event + ], + topic0 + ) -> + truncate_address_hash(Enum.at(event["topics"], 2)) + + Enum.member?([@transfer_single_event, @transfer_batch_event], topic0) -> + truncate_address_hash(Enum.at(event["topics"], 3)) + end + end + + defp get_op_amounts(topic0, event) do + cond do + topic0 == @new_deposit_block_event -> + [amount_or_nft_id, deposit_block_id] = decode_data(event["data"], [{:uint, 256}, {:uint, 256}]) + {[amount_or_nft_id], deposit_block_id} + + topic0 == @transfer_event -> + indexed_token_id = Enum.at(event["topics"], 3) + + if is_nil(indexed_token_id) do + {decode_data(event["data"], [{:uint, 256}]), 0} + else + {[quantity_to_integer(indexed_token_id)], 0} + end + + Enum.member?( + [ + @locked_ether_event, + @locked_erc20_event, + @locked_erc721_event, + @withdraw_event, + @exited_ether_event + ], + topic0 + ) -> + {decode_data(event["data"], [{:uint, 256}]), 0} + + topic0 == @locked_erc721_batch_event -> + [ids] = decode_data(event["data"], [{:array, {:uint, 256}}]) + {ids, 0} + + true -> + {[nil], 0} + end + end + + defp get_op_erc1155_data(topic0, event) do + cond do + Enum.member?([@locked_batch_erc1155_event, @transfer_batch_event], topic0) -> + [ids, amounts] = decode_data(event["data"], [{:array, {:uint, 256}}, {:array, {:uint, 256}}]) + {ids, amounts} + + Enum.member?([@transfer_single_event], topic0) -> + [id, amount] = decode_data(event["data"], [{:uint, 256}, {:uint, 256}]) + {[id], [amount]} + + true -> + {[], []} + end + end + + defp deposit?(topic0) do + Enum.member?( + [ + @new_deposit_block_event, + @locked_ether_event, + @locked_erc20_event, + @locked_erc721_event, + @locked_erc721_batch_event, + @locked_batch_erc1155_event + ], + topic0 + ) + end + + defp json_rpc_named_arguments(rpc_url) do + [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: rpc_url, + http_options: [ + recv_timeout: :timer.minutes(10), + timeout: :timer.minutes(10), + hackney: [pool: :ethereum_jsonrpc] + ] + ] + ] + end + + defp prepare_operations(events, json_rpc_named_arguments) do + timestamps = + events + |> filter_deposit_events() + |> Helper.get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number()) + |> Enum.reduce(%{}, fn block, acc -> + block_number = quantity_to_integer(Map.get(block, "number")) + {:ok, timestamp} = DateTime.from_unix(quantity_to_integer(Map.get(block, "timestamp"))) + Map.put(acc, block_number, timestamp) + end) + + events + |> Enum.map(fn event -> + topic0 = Enum.at(event["topics"], 0) + + user = get_op_user(topic0, event) + {amounts_or_ids, operation_id} = get_op_amounts(topic0, event) + {erc1155_ids, erc1155_amounts} = get_op_erc1155_data(topic0, event) + + l1_block_number = quantity_to_integer(event["blockNumber"]) + + {operation_type, timestamp} = + if deposit?(topic0) do + {:deposit, Map.get(timestamps, l1_block_number)} + else + {:withdrawal, nil} + end + + token_type = + cond do + Enum.member?([@new_deposit_block_event, @withdraw_event], topic0) -> + "bone" + + Enum.member?([@locked_ether_event, @exited_ether_event], topic0) -> + "eth" + + true -> + "other" + end + + Enum.map(amounts_or_ids, fn amount_or_id -> + %{ + user: user, + amount_or_id: amount_or_id, + erc1155_ids: if(Enum.empty?(erc1155_ids), do: nil, else: erc1155_ids), + erc1155_amounts: if(Enum.empty?(erc1155_amounts), do: nil, else: erc1155_amounts), + l1_transaction_hash: event["transactionHash"], + l1_block_number: l1_block_number, + l2_transaction_hash: @empty_hash, + operation_hash: calc_operation_hash(user, amount_or_id, erc1155_ids, erc1155_amounts, operation_id), + operation_type: operation_type, + token_type: token_type, + timestamp: timestamp + } + end) + end) + |> List.flatten() + end + + defp pad_address_hash(address) do + "0x" <> + (address + |> String.trim_leading("0x") + |> String.pad_leading(64, "0")) + end + + defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do + "0x#{truncated_hash}" + end + + defp reorg_handle(reorg_block) do + {deleted_count, _} = + Repo.delete_all(from(sb in Bridge, where: sb.l1_block_number >= ^reorg_block and is_nil(sb.l2_transaction_hash))) + + {updated_count1, _} = + Repo.update_all( + from(sb in Bridge, + where: + sb.l1_block_number >= ^reorg_block and not is_nil(sb.l2_transaction_hash) and + sb.operation_type == :deposit + ), + set: [timestamp: nil] + ) + + {updated_count2, _} = + Repo.update_all( + from(sb in Bridge, where: sb.l1_block_number >= ^reorg_block and not is_nil(sb.l2_transaction_hash)), + set: [l1_transaction_hash: nil, l1_block_number: nil] + ) + + updated_count = max(updated_count1, updated_count2) + + if deleted_count > 0 or updated_count > 0 do + recalculate_cached_count() + + Logger.warning( + "As L1 reorg was detected, some rows with l1_block_number >= #{reorg_block} were affected (removed or updated) in the shibarium_bridge table. Number of removed rows: #{deleted_count}. Number of updated rows: >= #{updated_count}." + ) + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/shibarium/l2.ex b/apps/indexer/lib/indexer/fetcher/shibarium/l2.ex new file mode 100644 index 000000000000..b85e4b558694 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/shibarium/l2.ex @@ -0,0 +1,545 @@ +defmodule Indexer.Fetcher.Shibarium.L2 do + @moduledoc """ + Fills shibarium_bridge DB table. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + import EthereumJSONRPC, + only: [ + json_rpc: 2, + quantity_to_integer: 1, + request: 1 + ] + + import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] + + import Explorer.Helper, only: [decode_data: 2, parse_integer: 1] + + import Indexer.Fetcher.Shibarium.Helper, + only: [calc_operation_hash: 5, prepare_insert_items: 2, recalculate_cached_count: 0] + + alias EthereumJSONRPC.Block.ByNumber + alias EthereumJSONRPC.{Blocks, Logs, Receipt} + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Shibarium.Bridge + alias Indexer.Helper + alias Indexer.Transform.Addresses + + @eth_get_logs_range_size 100 + @fetcher_name :shibarium_bridge_l2 + @empty_hash "0x0000000000000000000000000000000000000000000000000000000000000000" + + # 32-byte signature of the event TokenDeposited(address indexed rootToken, address indexed childToken, address indexed user, uint256 amount, uint256 depositCount) + @token_deposited_event "0xec3afb067bce33c5a294470ec5b29e6759301cd3928550490c6d48816cdc2f5d" + + # 32-byte signature of the event Transfer(address indexed from, address indexed to, uint256 value) + @transfer_event "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + # 32-byte signature of the event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value) + @transfer_single_event "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62" + + # 32-byte signature of the event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values) + @transfer_batch_event "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb" + + # 32-byte signature of the event Withdraw(address indexed rootToken, address indexed from, uint256 amount, uint256, uint256) + @withdraw_event "0xebff2602b3f468259e1e99f613fed6691f3a6526effe6ef3e768ba7ae7a36c4f" + + # 4-byte signature of the method withdraw(uint256 amount) + @withdraw_method "0x2e1a7d4d" + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(args) do + json_rpc_named_arguments = args[:json_rpc_named_arguments] + {:ok, %{}, {:continue, json_rpc_named_arguments}} + end + + @impl GenServer + def handle_continue(json_rpc_named_arguments, _state) do + Logger.metadata(fetcher: @fetcher_name) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + Process.send_after(self(), :init_with_delay, 2000) + {:noreply, %{json_rpc_named_arguments: json_rpc_named_arguments}} + end + + @impl GenServer + def handle_info(:init_with_delay, %{json_rpc_named_arguments: json_rpc_named_arguments} = state) do + env = Application.get_all_env(:indexer)[__MODULE__] + + with {:start_block_undefined, false} <- {:start_block_undefined, is_nil(env[:start_block])}, + {:child_chain_address_is_valid, true} <- + {:child_chain_address_is_valid, Helper.address_correct?(env[:child_chain])}, + {:weth_address_is_valid, true} <- {:weth_address_is_valid, Helper.address_correct?(env[:weth])}, + {:bone_withdraw_address_is_valid, true} <- + {:bone_withdraw_address_is_valid, Helper.address_correct?(env[:bone_withdraw])}, + start_block = parse_integer(env[:start_block]), + false <- is_nil(start_block), + true <- start_block > 0, + {last_l2_block_number, last_l2_transaction_hash} <- get_last_l2_item(), + {:ok, latest_block} = Helper.get_block_number_by_tag("latest", json_rpc_named_arguments), + {:start_block_valid, true} <- + {:start_block_valid, + (start_block <= last_l2_block_number || last_l2_block_number == 0) && start_block <= latest_block}, + {:ok, last_l2_tx} <- Helper.get_transaction_by_hash(last_l2_transaction_hash, json_rpc_named_arguments), + {:l2_tx_not_found, false} <- {:l2_tx_not_found, !is_nil(last_l2_transaction_hash) && is_nil(last_l2_tx)} do + recalculate_cached_count() + + Process.send(self(), :continue, []) + + {:noreply, + %{ + start_block: max(start_block, last_l2_block_number), + latest_block: latest_block, + child_chain: String.downcase(env[:child_chain]), + weth: String.downcase(env[:weth]), + bone_withdraw: String.downcase(env[:bone_withdraw]), + json_rpc_named_arguments: json_rpc_named_arguments + }} + else + {:start_block_undefined, true} -> + # the process shouldn't start if the start block is not defined + {:stop, :normal, state} + + {:child_chain_address_is_valid, false} -> + Logger.error("ChildChain contract address is invalid or not defined.") + {:stop, :normal, state} + + {:weth_address_is_valid, false} -> + Logger.error("WETH contract address is invalid or not defined.") + {:stop, :normal, state} + + {:bone_withdraw_address_is_valid, false} -> + Logger.error("Bone Withdraw contract address is invalid or not defined.") + {:stop, :normal, state} + + {:start_block_valid, false} -> + Logger.error("Invalid L2 Start Block value. Please, check the value and shibarium_bridge table.") + {:stop, :normal, state} + + {:error, error_data} -> + Logger.error( + "Cannot get last L2 transaction by its hash or latest block from RPC due to RPC error: #{inspect(error_data)}" + ) + + {:stop, :normal, state} + + {:l2_tx_not_found, true} -> + Logger.error( + "Cannot find last L2 transaction from RPC by its hash. Probably, there was a reorg on L2 chain. Please, check shibarium_bridge table." + ) + + {:stop, :normal, state} + + _ -> + Logger.error("L2 Start Block is invalid or zero.") + {:stop, :normal, state} + end + end + + @impl GenServer + def handle_info( + :continue, + %{ + start_block: start_block, + latest_block: end_block, + child_chain: child_chain, + weth: weth, + bone_withdraw: bone_withdraw, + json_rpc_named_arguments: json_rpc_named_arguments + } = state + ) do + start_block..end_block + |> Enum.chunk_every(@eth_get_logs_range_size) + |> Enum.each(fn current_chunk -> + chunk_start = List.first(current_chunk) + chunk_end = List.last(current_chunk) + + Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L2) + + operations = + chunk_start..chunk_end + |> get_logs_all(child_chain, bone_withdraw, json_rpc_named_arguments) + |> prepare_operations(weth) + + insert_items = prepare_insert_items(operations, __MODULE__) + + addresses = + Addresses.extract_addresses(%{ + shibarium_bridge_operations: insert_items + }) + + {:ok, _} = + Chain.import(%{ + addresses: %{params: addresses, on_conflict: :nothing}, + shibarium_bridge_operations: %{params: insert_items}, + timeout: :infinity + }) + + Helper.log_blocks_chunk_handling( + chunk_start, + chunk_end, + start_block, + end_block, + "#{Enum.count(operations)} L2 operation(s)", + :L2 + ) + end) + + {:stop, :normal, state} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + def filter_deposit_events(events, child_chain) do + Enum.filter(events, fn event -> + address = String.downcase(event.address_hash) + first_topic = Helper.log_topic_to_string(event.first_topic) + second_topic = Helper.log_topic_to_string(event.second_topic) + third_topic = Helper.log_topic_to_string(event.third_topic) + fourth_topic = Helper.log_topic_to_string(event.fourth_topic) + + (first_topic == @token_deposited_event and address == child_chain) or + (first_topic == @transfer_event and second_topic == @empty_hash and third_topic != @empty_hash) or + (Enum.member?([@transfer_single_event, @transfer_batch_event], first_topic) and + third_topic == @empty_hash and fourth_topic != @empty_hash) + end) + end + + def filter_withdrawal_events(events, bone_withdraw) do + Enum.filter(events, fn event -> + address = String.downcase(event.address_hash) + first_topic = Helper.log_topic_to_string(event.first_topic) + second_topic = Helper.log_topic_to_string(event.second_topic) + third_topic = Helper.log_topic_to_string(event.third_topic) + fourth_topic = Helper.log_topic_to_string(event.fourth_topic) + + (first_topic == @withdraw_event and address == bone_withdraw) or + (first_topic == @transfer_event and second_topic != @empty_hash and third_topic == @empty_hash) or + (Enum.member?([@transfer_single_event, @transfer_batch_event], first_topic) and + third_topic != @empty_hash and fourth_topic == @empty_hash) + end) + end + + def prepare_operations({events, timestamps}, weth) do + events + |> Enum.map(&prepare_operation(&1, timestamps, weth)) + |> List.flatten() + end + + def reorg_handle(reorg_block) do + {deleted_count, _} = + Repo.delete_all(from(sb in Bridge, where: sb.l2_block_number >= ^reorg_block and is_nil(sb.l1_transaction_hash))) + + {updated_count1, _} = + Repo.update_all( + from(sb in Bridge, + where: + sb.l2_block_number >= ^reorg_block and not is_nil(sb.l1_transaction_hash) and + sb.operation_type == :withdrawal + ), + set: [timestamp: nil] + ) + + {updated_count2, _} = + Repo.update_all( + from(sb in Bridge, where: sb.l2_block_number >= ^reorg_block and not is_nil(sb.l1_transaction_hash)), + set: [l2_transaction_hash: nil, l2_block_number: nil] + ) + + updated_count = max(updated_count1, updated_count2) + + if deleted_count > 0 or updated_count > 0 do + recalculate_cached_count() + + Logger.warning( + "As L2 reorg was detected, some rows with l2_block_number >= #{reorg_block} were affected (removed or updated) in the shibarium_bridge table. Number of removed rows: #{deleted_count}. Number of updated rows: >= #{updated_count}." + ) + end + end + + def withdraw_method_signature do + @withdraw_method + end + + defp get_blocks_by_range(range, json_rpc_named_arguments, retries) do + request = + range + |> Stream.map(fn block_number -> %{number: block_number} end) + |> Stream.with_index() + |> Enum.into(%{}, fn {params, id} -> {id, params} end) + |> Blocks.requests(&ByNumber.request(&1)) + + error_message = &"Cannot fetch blocks with batch request. Error: #{inspect(&1)}. Request: #{inspect(request)}" + + case Helper.repeated_call(&json_rpc/2, [request, json_rpc_named_arguments], error_message, retries) do + {:ok, results} -> Enum.map(results, fn %{result: result} -> result end) + {:error, _} -> [] + end + end + + defp get_last_l2_item do + query = + from(sb in Bridge, + select: {sb.l2_block_number, sb.l2_transaction_hash}, + where: not is_nil(sb.l2_block_number), + order_by: [desc: sb.l2_block_number], + limit: 1 + ) + + query + |> Repo.one() + |> Kernel.||({0, nil}) + end + + defp get_logs_all(block_range, child_chain, bone_withdraw, json_rpc_named_arguments) do + blocks = get_blocks_by_range(block_range, json_rpc_named_arguments, Helper.infinite_retries_number()) + + deposit_logs = get_deposit_logs_from_receipts(blocks, child_chain, json_rpc_named_arguments) + + withdrawal_logs = get_withdrawal_logs_from_receipts(blocks, bone_withdraw, json_rpc_named_arguments) + + timestamps = + blocks + |> Enum.reduce(%{}, fn block, acc -> + block_number = + block + |> Map.get("number") + |> quantity_to_integer() + + {:ok, timestamp} = + block + |> Map.get("timestamp") + |> quantity_to_integer() + |> DateTime.from_unix() + + Map.put(acc, block_number, timestamp) + end) + + {deposit_logs ++ withdrawal_logs, timestamps} + end + + defp get_deposit_logs_from_receipts(blocks, child_chain, json_rpc_named_arguments) do + blocks + |> Enum.reduce([], fn block, acc -> + hashes = + block + |> Map.get("transactions", []) + |> Enum.filter(fn t -> Map.get(t, "from") == burn_address_hash_string() end) + |> Enum.map(fn t -> Map.get(t, "hash") end) + + acc ++ hashes + end) + |> Enum.chunk_every(@eth_get_logs_range_size) + |> Enum.reduce([], fn hashes, acc -> + acc ++ get_receipt_logs(hashes, json_rpc_named_arguments, Helper.infinite_retries_number()) + end) + |> filter_deposit_events(child_chain) + end + + defp get_withdrawal_logs_from_receipts(blocks, bone_withdraw, json_rpc_named_arguments) do + blocks + |> Enum.reduce([], fn block, acc -> + hashes = + block + |> Map.get("transactions", []) + |> Enum.filter(fn t -> + # filter by `withdraw(uint256 amount)` signature + String.downcase(String.slice(Map.get(t, "input", ""), 0..9)) == @withdraw_method + end) + |> Enum.map(fn t -> Map.get(t, "hash") end) + + acc ++ hashes + end) + |> Enum.chunk_every(@eth_get_logs_range_size) + |> Enum.reduce([], fn hashes, acc -> + acc ++ get_receipt_logs(hashes, json_rpc_named_arguments, Helper.infinite_retries_number()) + end) + |> filter_withdrawal_events(bone_withdraw) + end + + defp get_op_amounts(event) do + cond do + event.first_topic == @token_deposited_event -> + [amount, deposit_count] = decode_data(event.data, [{:uint, 256}, {:uint, 256}]) + {[amount], deposit_count} + + event.first_topic == @transfer_event -> + indexed_token_id = event.fourth_topic + + if is_nil(indexed_token_id) do + {decode_data(event.data, [{:uint, 256}]), 0} + else + {[quantity_to_integer(indexed_token_id)], 0} + end + + event.first_topic == @withdraw_event -> + [amount, _arg3, _arg4] = decode_data(event.data, [{:uint, 256}, {:uint, 256}, {:uint, 256}]) + {[amount], 0} + + true -> + {[nil], 0} + end + end + + defp get_op_erc1155_data(event) do + cond do + event.first_topic == @transfer_single_event -> + [id, amount] = decode_data(event.data, [{:uint, 256}, {:uint, 256}]) + {[id], [amount]} + + event.first_topic == @transfer_batch_event -> + [ids, amounts] = decode_data(event.data, [{:array, {:uint, 256}}, {:array, {:uint, 256}}]) + {ids, amounts} + + true -> + {[], []} + end + end + + # credo:disable-for-next-line /Complexity/ + defp get_op_user(event) do + cond do + event.first_topic == @transfer_event and event.third_topic == @empty_hash -> + truncate_address_hash(event.second_topic) + + event.first_topic == @transfer_event and event.second_topic == @empty_hash -> + truncate_address_hash(event.third_topic) + + event.first_topic == @withdraw_event -> + truncate_address_hash(event.third_topic) + + Enum.member?([@transfer_single_event, @transfer_batch_event], event.first_topic) and + event.fourth_topic == @empty_hash -> + truncate_address_hash(event.third_topic) + + Enum.member?([@transfer_single_event, @transfer_batch_event], event.first_topic) and + event.third_topic == @empty_hash -> + truncate_address_hash(event.fourth_topic) + + event.first_topic == @token_deposited_event -> + truncate_address_hash(event.fourth_topic) + end + end + + defp get_receipt_logs(tx_hashes, json_rpc_named_arguments, retries) do + reqs = + tx_hashes + |> Enum.with_index() + |> Enum.map(fn {hash, id} -> + request(%{ + id: id, + method: "eth_getTransactionReceipt", + params: [hash] + }) + end) + + error_message = &"eth_getTransactionReceipt failed. Error: #{inspect(&1)}" + + {:ok, receipts} = Helper.repeated_call(&json_rpc/2, [reqs, json_rpc_named_arguments], error_message, retries) + + receipts + |> Enum.map(&Receipt.elixir_to_logs(&1.result)) + |> List.flatten() + |> Logs.elixir_to_params() + end + + defp withdrawal?(event) do + cond do + event.first_topic == @withdraw_event -> + true + + event.first_topic == @transfer_event and event.third_topic == @empty_hash -> + true + + Enum.member?([@transfer_single_event, @transfer_batch_event], event.first_topic) and + event.fourth_topic == @empty_hash -> + true + + true -> + false + end + end + + defp prepare_operation(event, timestamps, weth) do + event = + event + |> Map.put(:first_topic, Helper.log_topic_to_string(event.first_topic)) + |> Map.put(:second_topic, Helper.log_topic_to_string(event.second_topic)) + |> Map.put(:third_topic, Helper.log_topic_to_string(event.third_topic)) + |> Map.put(:fourth_topic, Helper.log_topic_to_string(event.fourth_topic)) + + user = get_op_user(event) + + if user == burn_address_hash_string() do + [] + else + {amounts_or_ids, operation_id} = get_op_amounts(event) + {erc1155_ids, erc1155_amounts} = get_op_erc1155_data(event) + + l2_block_number = quantity_to_integer(event.block_number) + + {operation_type, timestamp} = + if withdrawal?(event) do + {:withdrawal, Map.get(timestamps, l2_block_number)} + else + {:deposit, nil} + end + + token_type = + cond do + Enum.member?([@token_deposited_event, @withdraw_event], event.first_topic) -> + "bone" + + event.first_topic == @transfer_event and String.downcase(event.address_hash) == weth -> + "eth" + + true -> + "other" + end + + Enum.map(amounts_or_ids, fn amount_or_id -> + %{ + user: user, + amount_or_id: amount_or_id, + erc1155_ids: if(Enum.empty?(erc1155_ids), do: nil, else: erc1155_ids), + erc1155_amounts: if(Enum.empty?(erc1155_amounts), do: nil, else: erc1155_amounts), + l2_transaction_hash: event.transaction_hash, + l2_block_number: l2_block_number, + l1_transaction_hash: @empty_hash, + operation_hash: calc_operation_hash(user, amount_or_id, erc1155_ids, erc1155_amounts, operation_id), + operation_type: operation_type, + token_type: token_type, + timestamp: timestamp + } + end) + end + end + + defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do + "0x#{truncated_hash}" + end +end diff --git a/apps/indexer/lib/indexer/fetcher/stability/validator.ex b/apps/indexer/lib/indexer/fetcher/stability/validator.ex new file mode 100644 index 000000000000..70851e8d8a20 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/stability/validator.ex @@ -0,0 +1,70 @@ +defmodule Indexer.Fetcher.Stability.Validator do + @moduledoc """ + GenServer responsible for updating the list of stability validators in the database. + """ + use GenServer + + alias Explorer.Chain.Hash.Address, as: AddressHash + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def init(state) do + GenServer.cast(__MODULE__, :update_validators_list) + + {:ok, state} + end + + def handle_cast(:update_validators_list, state) do + validators_from_db = ValidatorStability.get_all_validators() + + case ValidatorStability.fetch_validators_lists() do + %{active: active_validator_addresses_list, all: validator_addresses_list} -> + validators_map = Enum.reduce(validator_addresses_list, %{}, fn address, map -> Map.put(map, address, true) end) + + active_validators_map = + Enum.reduce(active_validator_addresses_list, %{}, fn address, map -> Map.put(map, address, true) end) + + address_hashes_to_drop_from_db = + Enum.flat_map(validators_from_db, fn validator -> + (is_nil(validators_map[validator.address_hash.bytes]) && [validator.address_hash]) || [] + end) + + grouped = + Enum.group_by(validator_addresses_list, fn validator_address -> active_validators_map[validator_address] end) + + inactive = + Enum.map(grouped[nil] || [], fn address_hash -> + {:ok, address_hash} = AddressHash.load(address_hash) + + %{address_hash: address_hash, state: :inactive} |> ValidatorStability.append_timestamps() + end) + + validators_to_missing_blocks_numbers = ValidatorStability.fetch_missing_blocks_numbers(grouped[true] || []) + + active = + Enum.map(grouped[true] || [], fn address_hash_init -> + {:ok, address_hash} = AddressHash.load(address_hash_init) + + %{ + address_hash: address_hash, + state: + ValidatorStability.missing_block_number_to_state( + validators_to_missing_blocks_numbers[address_hash_init] + ) + } + |> ValidatorStability.append_timestamps() + end) + + ValidatorStability.insert_validators(active ++ inactive) + ValidatorStability.delete_validators_by_address_hashes(address_hashes_to_drop_from_db) + + _ -> + nil + end + + {:noreply, state} + end +end diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex index c9507315bc16..29b3bad04312 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex @@ -28,6 +28,8 @@ defmodule Indexer.Fetcher.TokenBalance do @default_max_batch_size 100 @default_max_concurrency 10 + @timeout :timer.minutes(10) + @max_retries 3 @spec async_fetch([ @@ -156,7 +158,7 @@ defmodule Indexer.Fetcher.TokenBalance do address_current_token_balances: %{ params: TokenBalances.to_address_current_token_balances(formatted_token_balances_params) }, - timeout: :infinity + timeout: @timeout } case Chain.import(import_params) do diff --git a/apps/indexer/lib/indexer/fetcher/token_balance_on_demand.ex b/apps/indexer/lib/indexer/fetcher/token_balance_on_demand.ex index d1dfcf6d3364..5ea891727eda 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance_on_demand.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance_on_demand.ex @@ -6,7 +6,7 @@ defmodule Indexer.Fetcher.TokenBalanceOnDemand do use Indexer.Fetcher - alias Explorer.Chain + alias Explorer.{Chain, Repo} alias Explorer.Chain.Address.CurrentTokenBalance alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Events.Publisher @@ -52,6 +52,7 @@ defmodule Indexer.Fetcher.TokenBalanceOnDemand do stale_current_token_balances = address_hash |> Chain.fetch_last_token_balances_include_unfetched() + |> delete_invalid_balances() |> Enum.filter(fn current_token_balance -> current_token_balance.block_number < stale_balance_window end) if Enum.count(stale_current_token_balances) > 0 do @@ -63,11 +64,24 @@ defmodule Indexer.Fetcher.TokenBalanceOnDemand do :ok end + defp delete_invalid_balances(current_token_balances) do + {invalid_balances, valid_balances} = Enum.split_with(current_token_balances, &is_nil(&1.token_type)) + Enum.each(invalid_balances, &Repo.delete/1) + valid_balances + end + defp fetch_and_update(block_number, address_hash, stale_current_token_balances) do - %{erc_1155: erc_1155_ctbs, other: other_ctbs, tokens: tokens} = + %{ + erc_1155: erc_1155_ctbs, + other: other_ctbs, + tokens: tokens, + balances_map: balances_map + } = stale_current_token_balances - |> Enum.reduce(%{erc_1155: [], other: [], tokens: %{}}, fn %{token_id: token_id} = stale_current_token_balance, - acc -> + |> Enum.reduce(%{erc_1155: [], other: [], tokens: %{}, balances_map: %{}}, fn %{ + token_id: token_id + } = stale_current_token_balance, + acc -> prepared_ctb = %{ token_contract_address_hash: "0x" <> Base.encode16(stale_current_token_balance.token.contract_address_hash.bytes), @@ -91,58 +105,82 @@ defmodule Indexer.Fetcher.TokenBalanceOnDemand do Map.put(acc, :other, [prepared_ctb | acc[:other]]) end - Map.put(result, :tokens, updated_tokens) - end) + updated_balances_map = + Map.put( + acc[:balances_map], + ctb_to_key(stale_current_token_balance), + stale_current_token_balance.value + ) - erc_1155_ctbs_reversed = Enum.reverse(erc_1155_ctbs) - other_ctbs_reversed = Enum.reverse(other_ctbs) + result + |> Map.put(:tokens, updated_tokens) + |> Map.put(:balances_map, updated_balances_map) + end) updated_erc_1155_ctbs = - if Enum.count(erc_1155_ctbs_reversed) > 0 do - erc_1155_ctbs_reversed + if Enum.count(erc_1155_ctbs) > 0 do + erc_1155_ctbs |> BalanceReader.get_balances_of_erc_1155() - |> Enum.zip(erc_1155_ctbs_reversed) + |> Enum.zip(erc_1155_ctbs) |> Enum.map(&prepare_updated_balance(&1, block_number)) else [] end updated_other_ctbs = - if Enum.count(other_ctbs_reversed) > 0 do - other_ctbs_reversed + if Enum.count(other_ctbs) > 0 do + other_ctbs |> BalanceReader.get_balances_of() - |> Enum.zip(other_ctbs_reversed) + |> Enum.zip(other_ctbs) |> Enum.map(&prepare_updated_balance(&1, block_number)) else [] end filtered_current_token_balances_update_params = - (updated_erc_1155_ctbs ++ updated_other_ctbs) - |> Enum.filter(&(!is_nil(&1))) - - {:ok, - %{ - address_current_token_balances: imported_ctbs - }} = - Chain.import(%{ - address_current_token_balances: %{ - params: filtered_current_token_balances_update_params + (updated_erc_1155_ctbs ++ updated_other_ctbs) |> Enum.filter(&(!is_nil(&1))) + + if Enum.count(filtered_current_token_balances_update_params) > 0 do + {:ok, + %{ + address_current_token_balances: imported_ctbs + }} = + Chain.import(%{ + address_current_token_balances: %{ + params: filtered_current_token_balances_update_params + }, + broadcast: false + }) + + filtered_imported_ctbs = filter_imported_ctbs(imported_ctbs, balances_map) + + Publisher.broadcast( + %{ + address_current_token_balances: %{ + address_hash: to_string(address_hash), + address_current_token_balances: + filtered_imported_ctbs + |> Enum.map(fn ctb -> %CurrentTokenBalance{ctb | token: tokens[ctb.token_contract_address_hash.bytes]} end) + } }, - broadcast: false - }) + :on_demand + ) + end + end - Publisher.broadcast( - %{ - address_current_token_balances: %{ - address_hash: to_string(address_hash), - address_current_token_balances: - imported_ctbs - |> Enum.map(fn ctb -> %CurrentTokenBalance{ctb | token: tokens[ctb.token_contract_address_hash.bytes]} end) - } - }, - :on_demand - ) + defp filter_imported_ctbs(imported_ctbs, balances_map) do + Enum.filter(imported_ctbs, fn ctb -> + if balance = balances_map[ctb_to_key(ctb)] do + Decimal.compare(balance, ctb.value) != :eq + else + Logger.error("Imported unknown balance") + true + end + end) + end + + defp ctb_to_key(ctb) do + {ctb.token_contract_address_hash.bytes, ctb.token_type, ctb.token_id && Decimal.to_integer(ctb.token_id)} end defp prepare_updated_balance({{:ok, updated_balance}, stale_current_token_balance}, block_number) do @@ -171,8 +209,18 @@ defmodule Indexer.Fetcher.TokenBalanceOnDemand do balance_response = case token_type do - "ERC-1155" -> BalanceReader.get_balances_of_erc_1155([request]) - _ -> BalanceReader.get_balances_of([request]) + "ERC-404" -> + if token_id do + BalanceReader.get_balances_of_erc_1155([request]) + else + BalanceReader.get_balances_of([request]) + end + + "ERC-1155" -> + BalanceReader.get_balances_of_erc_1155([request]) + + _ -> + BalanceReader.get_balances_of([request]) end balance = balance_response[:ok] diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex index e5e2256ff9eb..01dc381379db 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex @@ -6,12 +6,28 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do alias Explorer.SmartContract.Reader alias Indexer.Fetcher.TokenInstance.MetadataRetriever + require Logger + @cryptokitties_address_hash "0x06012c8cf97bead5deae237070f9587f8e7a266d" @token_uri "c87b56dd" + @base_uri "6c0360eb" @uri "0e89341c" @erc_721_1155_abi [ + %{ + "inputs" => [], + "name" => "baseURI", + "outputs" => [ + %{ + "internalType" => "string", + "name" => "", + "type" => "string" + } + ], + "stateMutability" => "view", + "type" => "function" + }, %{ "type" => "function", "stateMutability" => "view", @@ -79,16 +95,110 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do Map.put_new(acc, address_hash_string, Chain.get_token_type(contract_address_hash)) end) + {results, failed_results, instances_to_retry} = + other + |> batch_fetch_instances_inner(token_types_map, cryptokitties) + |> Enum.reduce({[], [], []}, fn {{_task, res}, {_result, _normalized_token_id, contract_address_hash, token_id}}, + {results, failed_results, instances_to_retry} -> + case res do + {:ok, {:error, "VM execution error"} = result} -> + add_failed_to_retry_result( + results, + failed_results, + instances_to_retry, + contract_address_hash, + token_id, + result + ) + + {:ok, result} -> + {[ + result_to_insert_params(result, contract_address_hash, token_id) + | results + ], failed_results, instances_to_retry} + + {:exit, reason} -> + {[ + result_to_insert_params( + {:error, MetadataRetriever.truncate_error("Terminated:" <> inspect(reason))}, + contract_address_hash, + token_id + ) + | results + ], failed_results, instances_to_retry} + end + end) + + total_results = + if Application.get_env(:indexer, __MODULE__)[:base_uri_retry?] do + {success_results_from_retry, failed_results_after_retry} = + instances_to_retry + |> batch_fetch_instances_inner(token_types_map, [], true) + |> Enum.reduce({[], []}, fn {{_task, res}, {_result, _normalized_token_id, contract_address_hash, token_id}}, + {success, failed} -> + # credo:disable-for-next-line + case res do + {:ok, result} -> + {[ + result_to_insert_params(result, contract_address_hash, token_id) + | success + ], failed} + + {:exit, reason} -> + { + success, + [ + result_to_insert_params( + {:error, MetadataRetriever.truncate_error("Terminated:" <> inspect(reason))}, + contract_address_hash, + token_id + ) + | failed + ] + } + end + end) + + results ++ success_results_from_retry ++ failed_results_after_retry + else + results ++ failed_results + end + + total_results + |> Enum.map(fn %{token_id: token_id, token_contract_address_hash: contract_address_hash} = result -> + upsert_with_rescue(result, token_id, contract_address_hash) + end) + end + + defp add_failed_to_retry_result(results, failed_results, instances_to_retry, contract_address_hash, token_id, result) do + { + results, + [ + result_to_insert_params(result, contract_address_hash, token_id) + | failed_results + ], + [{contract_address_hash, token_id} | instances_to_retry] + } + end + + defp batch_fetch_instances_inner(_token_instances, _token_types_map, _cryptokitties, from_base_uri? \\ false) + + defp batch_fetch_instances_inner(token_instances, token_types_map, cryptokitties, from_base_uri?) do contract_results = - (other + (token_instances |> Enum.map(fn {contract_address_hash, token_id} -> token_id = prepare_token_id(token_id) contract_address_hash_string = to_string(contract_address_hash) - prepare_request(token_types_map[contract_address_hash_string], contract_address_hash_string, token_id) + prepare_request( + token_types_map[contract_address_hash_string], + contract_address_hash_string, + token_id, + from_base_uri? + ) end) |> Reader.query_contracts(@erc_721_1155_abi, [], false) - |> Enum.zip_reduce(other, [], fn result, {contract_address_hash, token_id}, acc -> + |> Enum.zip_reduce(token_instances, [], fn result, {contract_address_hash, token_id}, acc -> token_id = prepare_token_id(token_id) [ @@ -101,40 +211,30 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do cryptokitties contract_results - |> Enum.map(fn {result, normalized_token_id, _contract_address_hash, _token_id} -> - Task.async(fn -> MetadataRetriever.fetch_json(result, normalized_token_id) end) + |> Enum.map(fn {result, normalized_token_id, _contract_address_hash, token_id} -> + Task.async(fn -> MetadataRetriever.fetch_json(result, token_id, normalized_token_id, from_base_uri?) end) end) |> Task.yield_many(:infinity) |> Enum.zip(contract_results) - |> Enum.map(fn {{_task, res}, {_result, _normalized_token_id, contract_address_hash, token_id}} -> - case res do - {:ok, result} -> - result_to_insert_params(result, contract_address_hash, token_id) - - {:exit, reason} -> - result_to_insert_params( - {:error, MetadataRetriever.truncate_error("Terminated:" <> inspect(reason))}, - contract_address_hash, - token_id - ) - end - end) - |> Chain.upsert_token_instances_list() end defp prepare_token_id(%Decimal{} = token_id), do: Decimal.to_integer(token_id) defp prepare_token_id(token_id), do: token_id - defp prepare_request("ERC-721", contract_address_hash_string, token_id) do - %{ + defp prepare_request("ERC-721", contract_address_hash_string, token_id, from_base_uri?) do + request = %{ contract_address: contract_address_hash_string, - method_id: @token_uri, - args: [token_id], block_number: nil } + + if from_base_uri? do + request |> Map.put(:method_id, @base_uri) |> Map.put(:args, []) + else + request |> Map.put(:method_id, @token_uri) |> Map.put(:args, [token_id]) + end end - defp prepare_request(_token_type, contract_address_hash_string, token_id) do + defp prepare_request(_token_type, contract_address_hash_string, token_id, _retry) do %{ contract_address: contract_address_hash_string, method_id: @uri, @@ -170,4 +270,21 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do error: error } end + + defp upsert_with_rescue(insert_params, token_id, token_contract_address_hash, retrying? \\ false) do + Chain.upsert_token_instance(insert_params) + rescue + error in Postgrex.Error -> + if retrying? do + Logger.warn(["Failed to upsert token instance: #{inspect(error)}"], fetcher: :token_instances) + nil + else + token_id + |> token_instance_map_with_error( + token_contract_address_hash, + MetadataRetriever.truncate_error(inspect(error.postgres.code)) + ) + |> upsert_with_rescue(token_id, token_contract_address_hash, true) + end + end end diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/legacy_sanitize.ex b/apps/indexer/lib/indexer/fetcher/token_instance/legacy_sanitize.ex index 1fe11e1d90dc..391353ba3788 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/legacy_sanitize.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/legacy_sanitize.ex @@ -1,58 +1,50 @@ defmodule Indexer.Fetcher.TokenInstance.LegacySanitize do @moduledoc """ - This fetcher is stands for creating token instances which wasn't inserted yet and index meta for them. Legacy is because now we token instances inserted on block import and this fetcher is only for historical and unfetched for some reasons data + This fetcher is stands for creating token instances which wasn't inserted yet and index meta for them. + Legacy is because now we token instances inserted on block import and this fetcher is only for historical and unfetched for some reasons data """ - use Indexer.Fetcher, restart: :permanent - use Spandex.Decorators + use GenServer, restart: :transient - import Indexer.Fetcher.TokenInstance.Helper - - alias Explorer.Chain - alias Indexer.BufferedTask - - @behaviour BufferedTask + alias Explorer.Chain.Token.Instance + alias Explorer.Repo - @default_max_batch_size 10 - @default_max_concurrency 10 - @doc false - def child_spec([init_options, gen_server_options]) do - merged_init_opts = - defaults() - |> Keyword.merge(init_options) - |> Keyword.merge(state: []) + import Indexer.Fetcher.TokenInstance.Helper - Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + def start_link(_) do + concurrency = Application.get_env(:indexer, __MODULE__)[:concurrency] + batch_size = Application.get_env(:indexer, __MODULE__)[:batch_size] + GenServer.start_link(__MODULE__, %{concurrency: concurrency, batch_size: batch_size}, name: __MODULE__) end - @impl BufferedTask - def init(initial_acc, reducer, _) do - {:ok, acc} = - Chain.stream_not_inserted_token_instances(initial_acc, fn data, acc -> - reducer.(data, acc) - end) + @impl true + def init(opts) do + GenServer.cast(__MODULE__, :backfill) - acc + {:ok, opts} end - @impl BufferedTask - def run(token_instances, _) when is_list(token_instances) do - token_instances - |> Enum.filter(fn %{contract_address_hash: hash, token_id: token_id} -> - not Chain.token_instance_exists?(token_id, hash) - end) - |> batch_fetch_instances() - - :ok + @impl true + def handle_cast(:backfill, %{concurrency: concurrency, batch_size: batch_size} = state) do + instances_to_fetch = + (concurrency * batch_size) + |> Instance.not_inserted_token_instances_query() + |> Repo.all() + + if Enum.empty?(instances_to_fetch) do + {:stop, :normal, state} + else + instances_to_fetch + |> Enum.uniq() + |> Enum.chunk_every(batch_size) + |> Enum.map(&process_batch/1) + |> Task.await_many(:infinity) + + GenServer.cast(__MODULE__, :backfill) + + {:noreply, state} + end end - defp defaults do - [ - flush_interval: :infinity, - max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, - max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, - poll: false, - task_supervisor: __MODULE__.TaskSupervisor - ] - end + defp process_batch(batch), do: Task.async(fn -> batch_fetch_instances(batch) end) end diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex b/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex index 975b7bed5f87..2f4c5eedb7d2 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex @@ -1,10 +1,11 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do @moduledoc """ - Fetches ERC-721 & ERC-1155 token instance metadata. + Fetches ERC-721/ERC-1155/ERC-404 token instance metadata. """ require Logger + alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.Reader alias HTTPoison.{Error, Response} @@ -35,24 +36,25 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do @doc """ Fetch/parse metadata using smart-contract's response """ - @spec fetch_json(any, binary() | nil) :: {:error, binary} | {:error_code, any} | {:ok, %{metadata: any}} - def fetch_json(uri, hex_token_id \\ nil) + @spec fetch_json(any, binary() | nil, binary() | nil, boolean) :: + {:error, binary} | {:error_code, any} | {:ok, %{metadata: any}} + def fetch_json(uri, token_id \\ nil, hex_token_id \\ nil, from_base_uri? \\ false) - def fetch_json(uri, _hex_token_id) when uri in [{:ok, [""]}, {:ok, [""]}] do + def fetch_json(uri, _token_id, _hex_token_id, _from_base_uri?) when uri in [{:ok, [""]}, {:ok, [""]}] do {:error, @no_uri_error} end - def fetch_json(uri, hex_token_id) do - fetch_json_from_uri(uri, hex_token_id) + def fetch_json(uri, token_id, hex_token_id, from_base_uri?) do + fetch_json_from_uri(uri, token_id, hex_token_id, from_base_uri?) end - defp fetch_json_from_uri({:error, error}, _hex_token_id) do + defp fetch_json_from_uri({:error, error}, _token_id, _hex_token_id, _from_base_uri?) do error = to_string(error) if error =~ "execution reverted" or error =~ @vm_execution_error do {:error, @vm_execution_error} else - Logger.debug(["Unknown metadata format error #{inspect(error)}."], fetcher: :token_instances) + Logger.warn(["Unknown metadata format error #{inspect(error)}."], fetcher: :token_instances) # truncate error since it will be stored in DB {:error, truncate_error(error)} @@ -60,53 +62,58 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do end # CIDv0 IPFS links # https://docs.ipfs.tech/concepts/content-addressing/#version-0-v0 - defp fetch_json_from_uri({:ok, ["Qm" <> _ = result]}, hex_token_id) do + defp fetch_json_from_uri({:ok, ["Qm" <> _ = result]}, token_id, hex_token_id, from_base_uri?) do if String.length(result) == 46 do - fetch_json_from_uri({:ok, [ipfs_link() <> result]}, hex_token_id) + fetch_json_from_uri({:ok, [ipfs_link() <> result]}, token_id, hex_token_id, from_base_uri?) else - Logger.debug(["Unknown metadata format result #{inspect(result)}."], fetcher: :token_instances) + Logger.warn(["Unknown metadata format result #{inspect(result)}."], fetcher: :token_instances) {:error, truncate_error(result)} end end - defp fetch_json_from_uri({:ok, ["'" <> token_uri]}, hex_token_id) do + defp fetch_json_from_uri({:ok, ["'" <> token_uri]}, token_id, hex_token_id, from_base_uri?) do token_uri = token_uri |> String.split("'") |> List.first() - fetch_metadata_inner(token_uri, hex_token_id) + fetch_metadata_inner(token_uri, token_id, hex_token_id, from_base_uri?) end - defp fetch_json_from_uri({:ok, ["http://" <> _ = token_uri]}, hex_token_id) do - fetch_metadata_inner(token_uri, hex_token_id) + defp fetch_json_from_uri({:ok, ["http://" <> _ = token_uri]}, token_id, hex_token_id, from_base_uri?) do + fetch_metadata_inner(token_uri, token_id, hex_token_id, from_base_uri?) end - defp fetch_json_from_uri({:ok, ["https://" <> _ = token_uri]}, hex_token_id) do - fetch_metadata_inner(token_uri, hex_token_id) + defp fetch_json_from_uri({:ok, ["https://" <> _ = token_uri]}, token_id, hex_token_id, from_base_uri?) do + fetch_metadata_inner(token_uri, token_id, hex_token_id, from_base_uri?) end - defp fetch_json_from_uri({:ok, ["data:application/json," <> json]}, hex_token_id) do + defp fetch_json_from_uri({:ok, ["data:application/json," <> json]}, token_id, hex_token_id, from_base_uri?) do decoded_json = URI.decode(json) - fetch_json_from_uri({:ok, [decoded_json]}, hex_token_id) + fetch_json_from_uri({:ok, [decoded_json]}, token_id, hex_token_id, from_base_uri?) rescue e -> - Logger.debug(["Unknown metadata format #{inspect(json)}.", Exception.format(:error, e, __STACKTRACE__)], + Logger.warn(["Unknown metadata format #{inspect(json)}.", Exception.format(:error, e, __STACKTRACE__)], fetcher: :token_instances ) {:error, "invalid data:application/json"} end - defp fetch_json_from_uri({:ok, ["data:application/json;base64," <> base64_encoded_json]}, hex_token_id) do + defp fetch_json_from_uri( + {:ok, ["data:application/json;base64," <> base64_encoded_json]}, + token_id, + hex_token_id, + from_base_uri? + ) do case Base.decode64(base64_encoded_json) do {:ok, base64_decoded} -> - fetch_json_from_uri({:ok, [base64_decoded]}, hex_token_id) + fetch_json_from_uri({:ok, [base64_decoded]}, token_id, hex_token_id, from_base_uri?) _ -> {:error, "invalid data:application/json;base64"} end rescue e -> - Logger.debug( + Logger.warn( [ "Unknown metadata format base64 #{inspect(base64_encoded_json)}.", Exception.format(:error, e, __STACKTRACE__) @@ -117,48 +124,50 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do {:error, "invalid data:application/json;base64"} end - defp fetch_json_from_uri({:ok, ["#{@ipfs_protocol}ipfs/" <> right]}, hex_token_id) do + defp fetch_json_from_uri({:ok, ["#{@ipfs_protocol}ipfs/" <> right]}, _token_id, hex_token_id, _from_base_uri?) do fetch_from_ipfs(right, hex_token_id) end - defp fetch_json_from_uri({:ok, ["ipfs/" <> right]}, hex_token_id) do + defp fetch_json_from_uri({:ok, ["ipfs/" <> right]}, _token_id, hex_token_id, _from_base_uri?) do fetch_from_ipfs(right, hex_token_id) end - defp fetch_json_from_uri({:ok, [@ipfs_protocol <> right]}, hex_token_id) do + defp fetch_json_from_uri({:ok, [@ipfs_protocol <> right]}, _token_id, hex_token_id, _from_base_uri?) do fetch_from_ipfs(right, hex_token_id) end - defp fetch_json_from_uri({:ok, [json]}, hex_token_id) do - {:ok, json} = decode_json(json) + defp fetch_json_from_uri({:ok, [json]}, _token_id, hex_token_id, _from_base_uri?) do + json = ExplorerHelper.decode_json(json) check_type(json, hex_token_id) rescue e -> - Logger.debug(["Unknown metadata format #{inspect(json)}.", Exception.format(:error, e, __STACKTRACE__)], + Logger.warn(["Unknown metadata format #{inspect(json)}.", Exception.format(:error, e, __STACKTRACE__)], fetcher: :token_instances ) {:error, "invalid json"} end - defp fetch_json_from_uri(uri, _hex_token_id) do - Logger.debug(["Unknown metadata uri format #{inspect(uri)}."], fetcher: :token_instances) + defp fetch_json_from_uri(uri, _token_id, _hex_token_id, _from_base_uri?) do + Logger.warn(["Unknown metadata uri format #{inspect(uri)}."], fetcher: :token_instances) {:error, "unknown metadata uri format"} end defp fetch_from_ipfs(ipfs_uid, hex_token_id) do ipfs_url = ipfs_link() <> ipfs_uid - fetch_metadata_inner(ipfs_url, hex_token_id) + fetch_metadata_inner(ipfs_url, nil, hex_token_id) end - defp fetch_metadata_inner(uri, hex_token_id) do - prepared_uri = substitute_token_id_to_token_uri(uri, hex_token_id) + defp fetch_metadata_inner(uri, token_id, hex_token_id, from_base_uri? \\ false) + + defp fetch_metadata_inner(uri, token_id, hex_token_id, from_base_uri?) do + prepared_uri = substitute_token_id_to_token_uri(uri, token_id, hex_token_id, from_base_uri?) fetch_metadata_from_uri(prepared_uri, hex_token_id) rescue e -> - Logger.debug( + Logger.warn( ["Could not prepare token uri #{inspect(uri)}.", Exception.format(:error, e, __STACKTRACE__)], fetcher: :token_instances ) @@ -196,7 +205,7 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do {:error_code, code} {:error, %Error{reason: reason}} -> - Logger.debug( + Logger.warn( ["Request to token uri failed: #{inspect(uri)}.", inspect(reason)], fetcher: :token_instances ) @@ -205,7 +214,7 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do end rescue e -> - Logger.debug( + Logger.warn( ["Could not send request to token uri #{inspect(uri)}.", Exception.format(:error, e, __STACKTRACE__)], fetcher: :token_instances ) @@ -214,15 +223,15 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do end defp check_content_type(content_type, uri, hex_token_id, body) do - image = is_image?(content_type) - video = is_video?(content_type) + image = image?(content_type) + video = video?(content_type) if content_type && (image || video) do json = if image, do: %{"image" => uri}, else: %{"animation_url" => uri} check_type(json, nil) else - {:ok, json} = decode_json(body) + json = ExplorerHelper.decode_json(body) check_type(json, hex_token_id) end @@ -237,24 +246,14 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do content_type end - defp is_image?(content_type) do + defp image?(content_type) do content_type && String.starts_with?(content_type, "image/") end - defp is_video?(content_type) do + defp video?(content_type) do content_type && String.starts_with?(content_type, "video/") end - defp decode_json(body) do - if String.valid?(body) do - Jason.decode(body) - else - body - |> :unicode.characters_to_binary(:latin1) - |> Jason.decode() - end - end - defp check_type(json, nil) when is_map(json) do {:ok, %{metadata: json}} end @@ -279,9 +278,19 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do {:error, "wrong metadata type"} end - defp substitute_token_id_to_token_uri(token_uri, empty_token_id) when empty_token_id in [nil, ""], do: token_uri + defp substitute_token_id_to_token_uri(base_uri, token_id, _empty_token_id, true) do + if String.ends_with?(base_uri, "/") do + base_uri <> to_string(token_id) + else + base_uri <> "/" <> to_string(token_id) + end + end + + defp substitute_token_id_to_token_uri(token_uri, _token_id, empty_token_id, _from_base_uri?) + when empty_token_id in [nil, ""], + do: token_uri - defp substitute_token_id_to_token_uri(token_uri, hex_token_id) do + defp substitute_token_id_to_token_uri(token_uri, _token_id, hex_token_id, _from_base_uri?) do String.replace(token_uri, @erc1155_token_id_placeholder, hex_token_id) end diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex new file mode 100644 index 000000000000..f2adfa8ac090 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex @@ -0,0 +1,51 @@ +defmodule Indexer.Fetcher.TokenInstance.SanitizeERC1155 do + @moduledoc """ + This fetcher is stands for creating token instances which wasn't inserted yet and index meta for them. + + !!!Imports only ERC-1155 token instances!!! + """ + + use GenServer, restart: :transient + + alias Explorer.Chain.Token.Instance + alias Explorer.Repo + + import Indexer.Fetcher.TokenInstance.Helper + + def start_link(_) do + concurrency = Application.get_env(:indexer, __MODULE__)[:concurrency] + batch_size = Application.get_env(:indexer, __MODULE__)[:batch_size] + GenServer.start_link(__MODULE__, %{concurrency: concurrency, batch_size: batch_size}, name: __MODULE__) + end + + @impl true + def init(opts) do + GenServer.cast(__MODULE__, :backfill) + + {:ok, opts} + end + + @impl true + def handle_cast(:backfill, %{concurrency: concurrency, batch_size: batch_size} = state) do + instances_to_fetch = + (concurrency * batch_size) + |> Instance.not_inserted_erc_1155_token_instances() + |> Repo.all() + + if Enum.empty?(instances_to_fetch) do + {:stop, :normal, state} + else + instances_to_fetch + |> Enum.uniq() + |> Enum.chunk_every(batch_size) + |> Enum.map(&process_batch/1) + |> Task.await_many(:infinity) + + GenServer.cast(__MODULE__, :backfill) + + {:noreply, state} + end + end + + defp process_batch(batch), do: Task.async(fn -> batch_fetch_instances(batch) end) +end diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex new file mode 100644 index 000000000000..bbe8bf7540b1 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex @@ -0,0 +1,89 @@ +defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721 do + @moduledoc """ + This fetcher is stands for creating token instances which wasn't inserted yet and index meta for them. + + !!!Imports only ERC-721 token instances!!! + """ + + use GenServer, restart: :transient + + alias Explorer.Application.Constants + alias Explorer.Chain.Token + alias Explorer.Chain.Token.Instance + alias Explorer.Repo + + import Indexer.Fetcher.TokenInstance.Helper + + def start_link(_) do + concurrency = Application.get_env(:indexer, __MODULE__)[:concurrency] + batch_size = Application.get_env(:indexer, __MODULE__)[:batch_size] + tokens_queue_size = Application.get_env(:indexer, __MODULE__)[:tokens_queue_size] + + GenServer.start_link( + __MODULE__, + %{concurrency: concurrency, batch_size: batch_size, tokens_queue_size: tokens_queue_size}, + name: __MODULE__ + ) + end + + @impl true + def init(opts) do + last_token_address_hash = Constants.get_last_processed_token_address_hash() + GenServer.cast(__MODULE__, :fetch_tokens_queue) + + {:ok, Map.put(opts, :last_token_address_hash, last_token_address_hash)} + end + + @impl true + def handle_cast(:fetch_tokens_queue, state) do + address_hashes = + state[:tokens_queue_size] + |> Token.ordered_erc_721_token_address_hashes_list_query(state[:last_token_address_hash]) + |> Repo.all() + + if Enum.empty?(address_hashes) do + {:stop, :normal, state} + else + GenServer.cast(__MODULE__, :backfill) + + {:noreply, Map.put(state, :tokens_queue, address_hashes)} + end + end + + @impl true + def handle_cast(:backfill, %{tokens_queue: []} = state) do + GenServer.cast(__MODULE__, :fetch_tokens_queue) + + {:noreply, state} + end + + @impl true + def handle_cast( + :backfill, + %{concurrency: concurrency, batch_size: batch_size, tokens_queue: [current_address_hash | remains]} = state + ) do + instances_to_fetch = + (concurrency * batch_size) + |> Instance.not_inserted_token_instances_query_by_token(current_address_hash) + |> Repo.all() + + if Enum.empty?(instances_to_fetch) do + Constants.insert_last_processed_token_address_hash(current_address_hash) + GenServer.cast(__MODULE__, :backfill) + + {:noreply, %{state | tokens_queue: remains, last_token_address_hash: current_address_hash}} + else + instances_to_fetch + |> Enum.uniq() + |> Enum.chunk_every(batch_size) + |> Enum.map(&process_batch/1) + |> Task.await_many(:infinity) + + GenServer.cast(__MODULE__, :backfill) + + {:noreply, state} + end + end + + defp process_batch(batch), do: Task.async(fn -> batch_fetch_instances(batch) end) +end diff --git a/apps/indexer/lib/indexer/fetcher/transaction_action.ex b/apps/indexer/lib/indexer/fetcher/transaction_action.ex index 6fddbf52a736..ac851b39992e 100644 --- a/apps/indexer/lib/indexer/fetcher/transaction_action.ex +++ b/apps/indexer/lib/indexer/fetcher/transaction_action.ex @@ -13,9 +13,10 @@ defmodule Indexer.Fetcher.TransactionAction do from: 2 ] + import Explorer.Helper, only: [parse_integer: 1] + alias Explorer.{Chain, Repo} - alias Explorer.Helper, as: ExplorerHelper - alias Explorer.Chain.{Block, Log, TransactionAction} + alias Explorer.Chain.{Block, BlockNumberHelper, Log, TransactionAction} alias Indexer.Transform.{Addresses, TransactionActions} @stage_first_block "tx_action_first_block" @@ -157,7 +158,7 @@ defmodule Indexer.Fetcher.TransactionAction do |> Decimal.round(2) |> Decimal.to_string() - next_block_new = block_number - 1 + next_block_new = BlockNumberHelper.previous_block_number(block_number) Logger.info( "Block #{block_number} handled successfully. Progress: #{progress_percentage}%. Initial block range: #{first_block}..#{last_block}." <> @@ -197,8 +198,8 @@ defmodule Indexer.Fetcher.TransactionAction do logger_metadata = Logger.metadata() Logger.metadata(fetcher: :transaction_action) - first_block = ExplorerHelper.parse_integer(first_block) - last_block = ExplorerHelper.parse_integer(last_block) + first_block = parse_integer(first_block) + last_block = parse_integer(last_block) return = if is_nil(first_block) or is_nil(last_block) or first_block <= 0 or last_block <= 0 or first_block > last_block do diff --git a/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex b/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex new file mode 100644 index 000000000000..74d7ec5b3bd8 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex @@ -0,0 +1,242 @@ +defmodule Indexer.Fetcher.ZkSync.BatchesStatusTracker do + @moduledoc """ + Updates batches statuses and imports historical batches to the `zksync_transaction_batches` table. + + Repetitiveness is supported by sending the following statuses every `recheck_interval` seconds: + - `:check_committed`: Discover batches committed to L1 + - `:check_proven`: Discover batches proven in L1 + - `:check_executed`: Discover batches executed on L1 + - `:recover_batches`: Recover missed batches found during the handling of the three previous messages + - `:check_historical`: Check if the imported batches chain does not start with Batch #0 + + The initial message is `:check_committed`. If it is discovered that updating batches + in the `zksync_transaction_batches` table is not possible because some are missing, + `:recover_batches` is sent. The next messages are `:check_proven` and `:check_executed`. + Both could result in sending `:recover_batches` as well. + + The logic ensures that every handler emits the `:recover_batches` message to return to + the previous "progressing" state. If `:recover_batches` is called during handling `:check_committed`, + it will be sent again after finishing batch recovery. Similar logic applies to `:check_proven` and + `:check_executed`. + + The last message in the loop is `:check_historical`. + + |---------------------------------------------------------------------------| + |-> check_committed -> check_proven -> check_executed -> check_historical ->| + | ^ | ^ | ^ + v | v | v | + recover_batches recover_batches recover_batches + + If a batch status change is discovered during handling of `check_committed`, `check_proven`, + or `check_executed` messages, the corresponding L1 transactions are imported and associated + with the batches. Rollup transactions and blocks are not re-associated since it is assumed + to be done by `Indexer.Fetcher.ZkSync.TransactionBatch` or during handling of + the `recover_batches` message. + + The `recover_batches` handler downloads batch information from RPC and sets its actual L1 state + by linking with L1 transactions. + + The `check_historical` message initiates the check if the tail of the batch chain is Batch 0. + If the tail is missing, batches are downloaded from RPC in chunks of `batches_max_range` in every + iteration. The batches are imported together with associated L1 transactions. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + # alias Explorer.Chain.Events.Publisher + # TODO: publish event when new committed batches appear + + alias Indexer.Fetcher.ZkSync.Discovery.Workers + alias Indexer.Fetcher.ZkSync.StatusTracking.{Committed, Executed, Proven} + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(args) do + Logger.metadata(fetcher: :zksync_batches_tracker) + + config_tracker = Application.get_all_env(:indexer)[Indexer.Fetcher.ZkSync.BatchesStatusTracker] + l1_rpc = config_tracker[:zksync_l1_rpc] + recheck_interval = config_tracker[:recheck_interval] + config_fetcher = Application.get_all_env(:indexer)[Indexer.Fetcher.ZkSync.TransactionBatch] + chunk_size = config_fetcher[:chunk_size] + batches_max_range = config_fetcher[:batches_max_range] + + Process.send(self(), :check_committed, []) + + {:ok, + %{ + config: %{ + json_l2_rpc_named_arguments: args[:json_rpc_named_arguments], + json_l1_rpc_named_arguments: [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: l1_rpc, + http_options: [ + recv_timeout: :timer.minutes(10), + timeout: :timer.minutes(10), + hackney: [pool: :ethereum_jsonrpc] + ] + ] + ], + recheck_interval: recheck_interval, + chunk_size: chunk_size, + batches_max_range: batches_max_range + }, + data: %{} + }} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + # Handles the `:check_historical` message to download historical batches from RPC if necessary and + # import them to the `zksync_transaction_batches` table. The batches are imported together with L1 + # transactions associations, rollup blocks and transactions. + # Since it is the final handler in the loop, it schedules sending the `:check_committed` message + # to initiate the next iteration. The sending of the message is delayed, taking into account + # the time remaining after the previous handlers' execution. + # + # ## Parameters + # - `:check_historical`: the message triggering the handler + # - `state`: current state of the fetcher containing both the fetcher configuration + # and data re-used by different handlers. + # + # ## Returns + # - `{:noreply, new_state}` where `new_state` contains `data` empty + @impl GenServer + def handle_info(:check_historical, state) + when is_map(state) and is_map_key(state, :config) and is_map_key(state, :data) and + is_map_key(state.config, :recheck_interval) and is_map_key(state.config, :batches_max_range) and + is_map_key(state.config, :json_l2_rpc_named_arguments) and + is_map_key(state.config, :chunk_size) do + {handle_duration, _} = + :timer.tc(&Workers.batches_catchup/1, [ + %{ + batches_max_range: state.config.batches_max_range, + chunk_size: state.config.chunk_size, + json_rpc_named_arguments: state.config.json_l2_rpc_named_arguments + } + ]) + + Process.send_after( + self(), + :check_committed, + max(:timer.seconds(state.config.recheck_interval) - div(update_duration(state.data, handle_duration), 1000), 0) + ) + + {:noreply, %{state | data: %{}}} + end + + # Handles the `:recover_batches` message to download a set of batches from RPC and imports them + # to the `zksync_transaction_batches` table. It is expected that the message is sent from handlers updating + # batches statuses when they discover the absence of batches in the `zksync_transaction_batches` table. + # The batches are imported together with L1 transactions associations, rollup blocks, and transactions. + # + # ## Parameters + # - `:recover_batches`: the message triggering the handler + # - `state`: current state of the fetcher containing both the fetcher configuration + # and data related to the batches recovery: + # - `state.data.batches`: list of the batches to recover + # - `state.data.switched_from`: the message to send after the batch recovery + # + # ## Returns + # - `{:noreply, new_state}` where `new_state` contains updated `duration` of the iteration + @impl GenServer + def handle_info(:recover_batches, state) + when is_map(state) and is_map_key(state, :config) and is_map_key(state, :data) and + is_map_key(state.config, :json_l2_rpc_named_arguments) and is_map_key(state.config, :chunk_size) and + is_map_key(state.data, :batches) and is_map_key(state.data, :switched_from) do + {handle_duration, _} = + :timer.tc( + &Workers.get_full_batches_info_and_import/2, + [ + state.data.batches, + %{ + chunk_size: state.config.chunk_size, + json_rpc_named_arguments: state.config.json_l2_rpc_named_arguments + } + ] + ) + + Process.send(self(), state.data.switched_from, []) + + {:noreply, %{state | data: %{duration: update_duration(state.data, handle_duration)}}} + end + + # Handles `:check_committed`, `:check_proven`, and `:check_executed` messages to update the + # statuses of batches by associating L1 transactions with them. For different messages, it invokes + # different underlying functions due to different natures of discovering batches with changed status. + # Another reason why statuses are being tracked differently is the different pace of status changes: + # a batch is committed in a few minutes after sealing, proven in a few hours, and executed once in a day. + # Depending on the value returned from the underlying function, either a message (`:check_proven`, + # `:check_executed`, or `:check_historical`) to switch to the next status checker is sent, or a list + # of batches to recover is provided together with `:recover_batches`. + # + # ## Parameters + # - `input`: one of `:check_committed`, `:check_proven`, and `:check_executed` + # - `state`: the current state of the fetcher containing both the fetcher configuration + # and data reused by different handlers. + # + # ## Returns + # - `{:noreply, new_state}` where `new_state` contains the updated `duration` of the iteration, + # could also contain the list of batches to recover and the message to return back to + # the corresponding status update checker. + @impl GenServer + def handle_info(input, state) + when input in [:check_committed, :check_proven, :check_executed] do + {output, func} = + case input do + :check_committed -> {:check_proven, &Committed.look_for_batches_and_update/1} + :check_proven -> {:check_executed, &Proven.look_for_batches_and_update/1} + :check_executed -> {:check_historical, &Executed.look_for_batches_and_update/1} + end + + {handle_duration, result} = :timer.tc(func, [state.config]) + + {switch_to, state_data} = + case result do + :ok -> + {output, %{duration: update_duration(state.data, handle_duration)}} + + {:recovery_required, batches} -> + {:recover_batches, + %{ + switched_from: input, + batches: batches, + duration: update_duration(state.data, handle_duration) + }} + end + + Process.send(self(), switch_to, []) + {:noreply, %{state | data: state_data}} + end + + defp update_duration(data, cur_duration) do + if Map.has_key?(data, :duration) do + data.duration + cur_duration + else + cur_duration + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/discovery/batches_data.ex b/apps/indexer/lib/indexer/fetcher/zksync/discovery/batches_data.ex new file mode 100644 index 000000000000..75b514ba74d0 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/discovery/batches_data.ex @@ -0,0 +1,413 @@ +defmodule Indexer.Fetcher.ZkSync.Discovery.BatchesData do + @moduledoc """ + Provides main functionality to extract data for batches and associated with them + rollup blocks, rollup and L1 transactions. + """ + + alias EthereumJSONRPC.Block.ByNumber + alias Indexer.Fetcher.ZkSync.Utils.Rpc + + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1, log_details_chunk_handling: 4] + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + @doc """ + Downloads batches, associates rollup blocks and transactions, and imports the results into the database. + Data is retrieved from the RPC endpoint in chunks of `chunk_size`. + + ## Parameters + - `batches`: Either a tuple of two integers, `start_batch_number` and `end_batch_number`, defining + the range of batches to receive, or a list of batch numbers, `batches_list`. + - `config`: Configuration containing `chunk_size` to limit the amount of data requested from the RPC endpoint, + and `json_rpc_named_arguments` defining parameters for the RPC connection. + + ## Returns + - `{batches_to_import, l2_blocks_to_import, l2_txs_to_import}` + where + - `batches_to_import` is a map of batches data + - `l2_blocks_to_import` is a list of blocks associated with batches by batch numbers + - `l2_txs_to_import` is a list of transactions associated with batches by batch numbers + """ + @spec extract_data_from_batches([integer()] | {integer(), integer()}, %{ + :chunk_size => pos_integer(), + :json_rpc_named_arguments => any(), + optional(any()) => any() + }) :: {map(), list(), list()} + def extract_data_from_batches(batches, config) + + def extract_data_from_batches({start_batch_number, end_batch_number}, config) + when is_integer(start_batch_number) and is_integer(end_batch_number) and + is_map(config) do + start_batch_number..end_batch_number + |> Enum.to_list() + |> do_extract_data_from_batches(config) + end + + def extract_data_from_batches(batches_list, config) + when is_list(batches_list) and + is_map(config) do + batches_list + |> do_extract_data_from_batches(config) + end + + defp do_extract_data_from_batches(batches_list, config) when is_list(batches_list) do + initial_batches_to_import = collect_batches_details(batches_list, config) + log_info("Collected details for #{length(Map.keys(initial_batches_to_import))} batches") + + batches_to_import = get_block_ranges(initial_batches_to_import, config) + + {l2_blocks_to_import, l2_txs_to_import} = get_l2_blocks_and_transactions(batches_to_import, config) + log_info("Linked #{length(l2_blocks_to_import)} L2 blocks and #{length(l2_txs_to_import)} L2 transactions") + + {batches_to_import, l2_blocks_to_import, l2_txs_to_import} + end + + @doc """ + Collects all unique L1 transactions from the given list of batches, including transactions + that change the status of a batch and their timestamps. + + **Note**: Every map describing an L1 transaction in the response is not ready for importing into + the database since it does not contain `:id` elements. + + ## Parameters + - `batches`: A list of maps describing batches. Each map is expected to define the following + elements: `commit_tx_hash`, `commit_timestamp`, `prove_tx_hash`, `prove_timestamp`, + `executed_tx_hash`, `executed_timestamp`. + + ## Returns + - `l1_txs`: A map where keys are L1 transaction hashes, and values are maps containing + transaction hashes and timestamps. + """ + @spec collect_l1_transactions(list()) :: map() + def collect_l1_transactions(batches) + when is_list(batches) do + l1_txs = + batches + |> Enum.reduce(%{}, fn batch, l1_txs -> + [ + %{hash: batch.commit_tx_hash, timestamp: batch.commit_timestamp}, + %{hash: batch.prove_tx_hash, timestamp: batch.prove_timestamp}, + %{hash: batch.executed_tx_hash, timestamp: batch.executed_timestamp} + ] + |> Enum.reduce(l1_txs, fn l1_tx, acc -> + # checks if l1_tx is not empty and adds to acc + add_l1_tx_to_list(acc, l1_tx) + end) + end) + + log_info("Collected #{length(Map.keys(l1_txs))} L1 hashes") + + l1_txs + end + + defp add_l1_tx_to_list(l1_txs, l1_tx) do + if l1_tx.hash != Rpc.get_binary_zero_hash() do + Map.put(l1_txs, l1_tx.hash, l1_tx) + else + l1_txs + end + end + + # Divides the list of batch numbers into chunks of size `chunk_size` to combine + # `zks_getL1BatchDetails` calls in one chunk together. To simplify further handling, + # each call is combined with the batch number in the JSON request identifier field. + # This allows parsing and associating every response with a particular batch, producing + # a list of maps describing the batches, ready for further handling. + # + # **Note**: The batches in the resulting map are not ready for importing into the DB. L1 transaction + # indices as well as the rollup blocks range must be added, and then batch descriptions + # must be pruned (see Indexer.Fetcher.ZkSync.Utils.Db.prune_json_batch/1). + # + # ## Parameters + # - `batches_list`: A list of batch numbers. + # - `config`: A map containing `chunk_size` specifying the number of `zks_getL1BatchDetails` in + # one HTTP request, and `json_rpc_named_arguments` describing parameters for + # RPC connection. + # + # ## Returns + # - `batches_details`: A map where keys are batch numbers, and values are maps produced + # after parsing responses of `zks_getL1BatchDetails` calls. + defp collect_batches_details( + batches_list, + %{json_rpc_named_arguments: json_rpc_named_arguments, chunk_size: chunk_size} = _config + ) + when is_list(batches_list) do + batches_list_length = length(batches_list) + + {batches_details, _} = + batches_list + |> Enum.chunk_every(chunk_size) + |> Enum.reduce({%{}, 0}, fn chunk, {details, a} -> + log_details_chunk_handling("Collecting details", chunk, a * chunk_size, batches_list_length) + + requests = + chunk + |> Enum.map(fn batch_number -> + EthereumJSONRPC.request(%{ + id: batch_number, + method: "zks_getL1BatchDetails", + params: [batch_number] + }) + end) + + details = + requests + |> Rpc.fetch_batches_details(json_rpc_named_arguments) + |> Enum.reduce( + details, + fn resp, details -> + Map.put(details, resp.id, Rpc.transform_batch_details_to_map(resp.result)) + end + ) + + {details, a + 1} + end) + + batches_details + end + + # Extends each batch description with the block numbers specifying the start and end of + # a range of blocks included in the batch. The block ranges are obtained through the RPC call + # `zks_getL1BatchBlockRange`. The calls are combined in chunks of `chunk_size`. To distinguish + # each call in the chunk, they are combined with the batch number in the JSON request + # identifier field. + # + # ## Parameters + # - `batches`: A map of batch descriptions. + # - `config`: A map containing `chunk_size`, specifying the number of `zks_getL1BatchBlockRange` + # in one HTTP request, and `json_rpc_named_arguments` describing parameters for + # RPC connection. + # + # ## Returns + # - `updated_batches`: A map of batch descriptions where each description is updated with + # a range (elements `:start_block` and `:end_block`) of rollup blocks included in the batch. + defp get_block_ranges( + batches, + %{json_rpc_named_arguments: json_rpc_named_arguments, chunk_size: chunk_size} = _config + ) + when is_map(batches) do + keys = Map.keys(batches) + batches_list_length = length(keys) + + {updated_batches, _} = + keys + |> Enum.chunk_every(chunk_size) + |> Enum.reduce({batches, 0}, fn batches_chunk, {batches_with_block_ranges, a} -> + log_details_chunk_handling("Collecting block ranges", batches_chunk, a * chunk_size, batches_list_length) + + {request_block_ranges_for_batches(batches_chunk, batches, batches_with_block_ranges, json_rpc_named_arguments), + a + 1} + end) + + updated_batches + end + + # For a given list of rollup batch numbers, this function builds a list of requests + # to `zks_getL1BatchBlockRange`, executes them, and extends the batches' descriptions with + # ranges of rollup blocks associated with each batch. + # + # ## Parameters + # - `batches_numbers`: A list with batch numbers. + # - `batches_src`: A list containing original batches descriptions. + # - `batches_dst`: A map with extended batch descriptions containing rollup block ranges. + # - `json_rpc_named_arguments`: Describes parameters for RPC connection. + # + # ## Returns + # - An updated version of `batches_dst` with new entities containing rollup block ranges. + defp request_block_ranges_for_batches(batches_numbers, batches_src, batches_dst, json_rpc_named_arguments) do + batches_numbers + |> Enum.reduce([], fn batch_number, requests -> + batch = Map.get(batches_src, batch_number) + # Prepare requests list to get blocks ranges + case is_nil(batch.start_block) or is_nil(batch.end_block) do + true -> + [ + EthereumJSONRPC.request(%{ + id: batch_number, + method: "zks_getL1BatchBlockRange", + params: [batch_number] + }) + | requests + ] + + false -> + requests + end + end) + |> Rpc.fetch_blocks_ranges(json_rpc_named_arguments) + |> Enum.reduce(batches_dst, fn resp, updated_batches -> + Map.update!(updated_batches, resp.id, fn batch -> + [start_block, end_block] = resp.result + + Map.merge(batch, %{ + start_block: quantity_to_integer(start_block), + end_block: quantity_to_integer(end_block) + }) + end) + end) + end + + # Unfolds the ranges of rollup blocks in each batch description, makes RPC `eth_getBlockByNumber` calls, + # and builds two lists: a list of rollup blocks associated with each batch and a list of rollup transactions + # associated with each batch. RPC calls are made in chunks of `chunk_size`. To distinguish + # each call in the chunk, they are combined with the block number in the JSON request + # identifier field. + # + # ## Parameters + # - `batches`: A map of batch descriptions. Each description must contain `start_block` and + # `end_block`, specifying the range of blocks associated with the batch. + # - `config`: A map containing `chunk_size`, specifying the number of `eth_getBlockByNumber` + # in one HTTP request, and `json_rpc_named_arguments` describing parameters for + # RPC connection. + # + # ## Returns + # - {l2_blocks_to_import, l2_txs_to_import}, where + # - `l2_blocks_to_import` contains a list of all rollup blocks with their associations with + # the provided batches. The association is a map with the block hash and the batch number. + # - `l2_txs_to_import` contains a list of all rollup transactions with their associations + # with the provided batches. The association is a map with the transaction hash and + # the batch number. + defp get_l2_blocks_and_transactions( + batches, + %{json_rpc_named_arguments: json_rpc_named_arguments, chunk_size: chunk_size} = _config + ) do + # Extracts the rollup block range for every batch, unfolds it and + # build chunks of `eth_getBlockByNumber` calls + {blocks_to_batches, chunked_requests, cur_chunk, cur_chunk_size} = + batches + |> Map.keys() + |> Enum.reduce({%{}, [], [], 0}, fn batch_number, cur_batch_acc -> + batch = Map.get(batches, batch_number) + + batch.start_block..batch.end_block + |> Enum.chunk_every(chunk_size) + |> Enum.reduce(cur_batch_acc, fn blocks_range, cur_chunk_acc -> + build_blocks_map_and_chunks_of_rpc_requests(batch_number, blocks_range, cur_chunk_acc, chunk_size) + end) + end) + + # After the last iteration of the reduce loop it is a valid case + # when the calls from the last chunk are not in the chunks list, + # so it is appended + finalized_chunked_requests = + if cur_chunk_size > 0 do + [cur_chunk | chunked_requests] + else + chunked_requests + end + + # The chunks requests are sent to the RPC node and parsed to + # extract rollup block hashes and rollup transactions. + {blocks_associations, l2_txs_to_import} = + finalized_chunked_requests + |> Enum.reduce({blocks_to_batches, []}, fn requests, {blocks, l2_txs} -> + requests + |> Rpc.fetch_blocks_details(json_rpc_named_arguments) + |> extract_block_hash_and_transactions_list(blocks, l2_txs) + end) + + # Check that amount of received transactions for a batch is correct + batches + |> Map.keys() + |> Enum.each(fn batch_number -> + batch = Map.get(batches, batch_number) + txs_in_batch = batch.l1_tx_count + batch.l2_tx_count + + ^txs_in_batch = + Enum.count(l2_txs_to_import, fn tx -> + tx.batch_number == batch_number + end) + end) + + {Map.values(blocks_associations), l2_txs_to_import} + end + + # For a given list of rollup block numbers, this function extends: + # - a map containing the linkage between rollup block numbers and batch numbers + # - a list of chunks of `eth_getBlockByNumber` requests + # - an uncompleted chunk of `eth_getBlockByNumber` requests + # + # ## Parameters + # - `batch_number`: The number of the batch to which the list of rollup blocks is linked. + # - `blocks_numbers`: A list of rollup block numbers. + # - `cur_chunk_acc`: The current state of the accumulator containing: + # - the current state of the map containing the linkage between rollup block numbers and batch numbers + # - the current state of the list of chunks of `eth_getBlockByNumber` requests + # - the current state of the uncompleted chunk of `eth_getBlockByNumber` requests + # - the size of the uncompleted chunk + # - `chunk_size`: The maximum size of the chunk of `eth_getBlockByNumber` requests + # + # ## Returns + # - {blocks_to_batches, chunked_requests, cur_chunk, cur_chunk_size}, where: + # - `blocks_to_batches`: An updated map with new blocks added. + # - `chunked_requests`: An updated list of lists of `eth_getBlockByNumber` requests. + # - `cur_chunk`: An uncompleted chunk of `eth_getBlockByNumber` requests or an empty list. + # - `cur_chunk_size`: The size of the uncompleted chunk. + defp build_blocks_map_and_chunks_of_rpc_requests(batch_number, blocks_numbers, cur_chunk_acc, chunk_size) do + blocks_numbers + |> Enum.reduce(cur_chunk_acc, fn block_number, {blocks_to_batches, chunked_requests, cur_chunk, cur_chunk_size} -> + blocks_to_batches = Map.put(blocks_to_batches, block_number, %{batch_number: batch_number}) + + cur_chunk = [ + ByNumber.request( + %{ + id: block_number, + number: block_number + }, + false + ) + | cur_chunk + ] + + if cur_chunk_size + 1 == chunk_size do + {blocks_to_batches, [cur_chunk | chunked_requests], [], 0} + else + {blocks_to_batches, chunked_requests, cur_chunk, cur_chunk_size + 1} + end + end) + end + + # Parses responses from `eth_getBlockByNumber` calls and extracts the block hash and the + # transactions lists. The block hash and transaction hashes are used to build associations + # with the corresponding batches by utilizing their numbers. + # + # This function is not part of the `Indexer.Fetcher.ZkSync.Utils.Rpc` module since the resulting + # lists are too specific for further import to the database. + # + # ## Parameters + # - `json_responses`: A list of responses to `eth_getBlockByNumber` calls. + # - `l2_blocks`: A map of accumulated associations between rollup blocks and batches. + # - `l2_txs`: A list of accumulated associations between rollup transactions and batches. + # + # ## Returns + # - {l2_blocks, l2_txs}, where + # - `l2_blocks`: Updated map of accumulated associations between rollup blocks and batches. + # - `l2_txs`: Updated list of accumulated associations between rollup transactions and batches. + defp extract_block_hash_and_transactions_list(json_responses, l2_blocks, l2_txs) do + json_responses + |> Enum.reduce({l2_blocks, l2_txs}, fn resp, {l2_blocks, l2_txs} -> + {block, l2_blocks} = + Map.get_and_update(l2_blocks, resp.id, fn block -> + {block, Map.put(block, :hash, Map.get(resp.result, "hash"))} + end) + + l2_txs = + case Map.get(resp.result, "transactions") do + nil -> + l2_txs + + new_txs -> + Enum.reduce(new_txs, l2_txs, fn l2_tx_hash, l2_txs -> + [ + %{ + batch_number: block.batch_number, + hash: l2_tx_hash + } + | l2_txs + ] + end) + end + + {l2_blocks, l2_txs} + end) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/discovery/workers.ex b/apps/indexer/lib/indexer/fetcher/zksync/discovery/workers.ex new file mode 100644 index 000000000000..43ad89b7f124 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/discovery/workers.ex @@ -0,0 +1,163 @@ +defmodule Indexer.Fetcher.ZkSync.Discovery.Workers do + @moduledoc """ + Provides functions to download a set of batches from RPC and import them to DB. + """ + + alias Indexer.Fetcher.ZkSync.Utils.Db + + import Indexer.Fetcher.ZkSync.Discovery.BatchesData, + only: [ + collect_l1_transactions: 1, + extract_data_from_batches: 2 + ] + + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1] + + @doc """ + Downloads minimal batches data (batch, associated rollup blocks and transactions hashes) from RPC + and imports them to the DB. Data is retrieved from the RPC endpoint in chunks of `chunk_size`. + Import of associated L1 transactions does not happen, assuming that the batch import happens regularly + enough and last downloaded batches does not contain L1 associations anyway. + Later `Indexer.Fetcher.ZkSync.BatchesStatusTracker` will update any batch state changes and + import required L1 transactions. + + ## Parameters + - `start_batch_number`: The first batch in the range to download. + - `end_batch_number`: The last batch in the range to download. + - `config`: Configuration containing `chunk_size` to limit the amount of data requested from the RPC endpoint, + and `json_rpc_named_arguments` defining parameters for the RPC connection. + + ## Returns + - `:ok` + """ + @spec get_minimal_batches_info_and_import(non_neg_integer(), non_neg_integer(), %{ + :chunk_size => integer(), + :json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }) :: :ok + def get_minimal_batches_info_and_import(start_batch_number, end_batch_number, config) + when is_integer(start_batch_number) and + is_integer(end_batch_number) and + (is_map(config) and is_map_key(config, :json_rpc_named_arguments) and + is_map_key(config, :chunk_size)) do + {batches_to_import, l2_blocks_to_import, l2_txs_to_import} = + extract_data_from_batches({start_batch_number, end_batch_number}, config) + + batches_list_to_import = + batches_to_import + |> Map.values() + |> Enum.reduce([], fn batch, batches_list -> + [Db.prune_json_batch(batch) | batches_list] + end) + + Db.import_to_db( + batches_list_to_import, + [], + l2_txs_to_import, + l2_blocks_to_import + ) + + :ok + end + + @doc """ + Downloads batches, associates L1 transactions, rollup blocks and transactions with the given list of batch numbers, + and imports the results into the database. Data is retrieved from the RPC endpoint in chunks of `chunk_size`. + + ## Parameters + - `batches_numbers_list`: List of batch numbers to be retrieved. + - `config`: Configuration containing `chunk_size` to limit the amount of data requested from the RPC endpoint, + and `json_rpc_named_arguments` defining parameters for the RPC connection. + + ## Returns + - `:ok` + """ + @spec get_full_batches_info_and_import([integer()], %{ + :chunk_size => integer(), + :json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }) :: :ok + def get_full_batches_info_and_import(batches_numbers_list, config) + when is_list(batches_numbers_list) and + (is_map(config) and is_map_key(config, :json_rpc_named_arguments) and + is_map_key(config, :chunk_size)) do + # Collect batches and linked L2 blocks and transaction + {batches_to_import, l2_blocks_to_import, l2_txs_to_import} = extract_data_from_batches(batches_numbers_list, config) + + # Collect L1 transactions associated with batches + l1_txs = + batches_to_import + |> Map.values() + |> collect_l1_transactions() + |> Db.get_indices_for_l1_transactions() + + # Update batches with l1 transactions indices and prune unnecessary fields + batches_list_to_import = + batches_to_import + |> Map.values() + |> Enum.reduce([], fn batch, batches -> + [ + batch + |> Map.put(:commit_id, get_l1_tx_id_by_hash(l1_txs, batch.commit_tx_hash)) + |> Map.put(:prove_id, get_l1_tx_id_by_hash(l1_txs, batch.prove_tx_hash)) + |> Map.put(:execute_id, get_l1_tx_id_by_hash(l1_txs, batch.executed_tx_hash)) + |> Db.prune_json_batch() + | batches + ] + end) + + Db.import_to_db( + batches_list_to_import, + Map.values(l1_txs), + l2_txs_to_import, + l2_blocks_to_import + ) + + :ok + end + + @doc """ + Retrieves the minimal batch number from the database. If the minimum batch number is not zero, + downloads `batches_max_range` batches older than the retrieved batch, along with associated + L1 transactions, rollup blocks, and transactions, and imports everything to the database. + + ## Parameters + - `config`: Configuration containing `chunk_size` to limit the amount of data requested from + the RPC endpoint and `json_rpc_named_arguments` defining parameters for the + RPC connection, `batches_max_range` defines how many of older batches must be downloaded. + + ## Returns + - `:ok` + """ + @spec batches_catchup(%{ + :batches_max_range => integer(), + :chunk_size => integer(), + :json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }) :: :ok + def batches_catchup(config) + when is_map(config) and is_map_key(config, :json_rpc_named_arguments) and + is_map_key(config, :batches_max_range) and + is_map_key(config, :chunk_size) do + oldest_batch_number = Db.get_earliest_batch_number() + + if not is_nil(oldest_batch_number) && oldest_batch_number > 0 do + log_info("The oldest batch number is not zero. Historical baches will be fetched.") + start_batch_number = max(0, oldest_batch_number - config.batches_max_range) + end_batch_number = oldest_batch_number - 1 + + start_batch_number..end_batch_number + |> Enum.to_list() + |> get_full_batches_info_and_import(config) + end + + :ok + end + + defp get_l1_tx_id_by_hash(l1_txs, hash) do + l1_txs + |> Map.get(hash) + |> Kernel.||(%{id: nil}) + |> Map.get(:id) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/committed.ex b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/committed.ex new file mode 100644 index 000000000000..ed1a0464b63c --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/committed.ex @@ -0,0 +1,78 @@ +defmodule Indexer.Fetcher.ZkSync.StatusTracking.Committed do + @moduledoc """ + Functionality to discover committed batches + """ + + alias Indexer.Fetcher.ZkSync.Utils.{Db, Rpc} + + import Indexer.Fetcher.ZkSync.StatusTracking.CommonUtils, + only: [ + check_if_batch_status_changed: 3, + associate_and_import_or_prepare_for_recovery: 4 + ] + + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1] + + # keccak256("BlockCommit(uint256,bytes32,bytes32)") + @block_commit_event "0x8f2916b2f2d78cc5890ead36c06c0f6d5d112c7e103589947e8e2f0d6eddb763" + + @doc """ + Checks if the oldest uncommitted batch in the database has the associated L1 commitment transaction + by requesting new batch details from RPC. If so, analyzes the `BlockCommit` event emitted by + the transaction to explore all the batches committed by it. For all discovered batches, it updates + the database with new associations, importing information about L1 transactions. + If it is found that some of the discovered batches are absent in the database, the function + interrupts and returns the list of batch numbers that can be attempted to be recovered. + + ## Parameters + - `config`: Configuration containing `json_l1_rpc_named_arguments` and + `json_l2_rpc_named_arguments` defining parameters for the RPC connections. + + ## Returns + - `:ok` if no new committed batches are found, or if all found batches and the corresponding L1 + transactions are imported successfully. + - `{:recovery_required, batches_to_recover}` if the absence of new committed batches is + discovered; `batches_to_recover` contains the list of batch numbers. + """ + @spec look_for_batches_and_update(%{ + :json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + :json_l2_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }) :: :ok | {:recovery_required, list()} + def look_for_batches_and_update( + %{ + json_l1_rpc_named_arguments: json_l1_rpc_named_arguments, + json_l2_rpc_named_arguments: json_l2_rpc_named_arguments + } = _config + ) do + case Db.get_earliest_sealed_batch_number() do + nil -> + :ok + + expected_batch_number -> + log_info("Checking if the batch #{expected_batch_number} was committed") + + {next_action, tx_hash, l1_txs} = + check_if_batch_status_changed(expected_batch_number, :commit_tx, json_l2_rpc_named_arguments) + + case next_action do + :skip -> + :ok + + :look_for_batches -> + log_info("The batch #{expected_batch_number} looks like committed") + commit_tx_receipt = Rpc.fetch_tx_receipt_by_hash(tx_hash, json_l1_rpc_named_arguments) + batches_numbers_from_rpc = get_committed_batches_from_logs(commit_tx_receipt["logs"]) + + associate_and_import_or_prepare_for_recovery(batches_numbers_from_rpc, l1_txs, tx_hash, :commit_id) + end + end + end + + defp get_committed_batches_from_logs(logs) do + committed_batches = Rpc.filter_logs_and_extract_topic_at(logs, @block_commit_event, 1) + log_info("Discovered #{length(committed_batches)} committed batches in the commitment tx") + + committed_batches + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/common.ex b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/common.ex new file mode 100644 index 000000000000..0c8cccffc30d --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/common.ex @@ -0,0 +1,173 @@ +defmodule Indexer.Fetcher.ZkSync.StatusTracking.CommonUtils do + @moduledoc """ + Common functions for status changes trackers + """ + + alias Explorer.Chain.ZkSync.Reader + alias Indexer.Fetcher.ZkSync.Utils.{Db, Rpc} + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_warning: 1] + + @doc """ + Fetches the details of the batch with the given number and checks if the representation of + the same batch in the database refers to the same commitment, proving, or executing transaction + depending on `tx_type`. If the transaction state changes, the new transaction is prepared for + import to the database. + + ## Parameters + - `batch_number`: the number of the batch to check L1 transaction state. + - `tx_type`: a type of the transaction to check, one of :commit_tx, :execute_tx, or :prove_tx. + - `json_l2_rpc_named_arguments`: parameters for the RPC connections. + + ## Returns + - `{:look_for_batches, l1_tx_hash, l1_txs}` where + - `l1_tx_hash` is the hash of the L1 transaction. + - `l1_txs` is a map containing the transaction hash as a key, and values are maps + with transaction hashes and transaction timestamps. + - `{:skip, "", %{}}` means the batch is not found in the database or the state of the transaction + in the batch representation is the same as the state of the transaction for the batch + received from RPC. + """ + @spec check_if_batch_status_changed( + binary() | non_neg_integer(), + :commit_tx | :execute_tx | :prove_tx, + EthereumJSONRPC.json_rpc_named_arguments() + ) :: {:look_for_batches, any(), any()} | {:skip, <<>>, %{}} + def check_if_batch_status_changed(batch_number, tx_type, json_l2_rpc_named_arguments) + when (is_binary(batch_number) or is_integer(batch_number)) and + tx_type in [:commit_tx, :prove_tx, :execute_tx] and + is_list(json_l2_rpc_named_arguments) do + batch_from_rpc = Rpc.fetch_batch_details_by_batch_number(batch_number, json_l2_rpc_named_arguments) + + status_changed_or_error = + case Reader.batch( + batch_number, + necessity_by_association: %{ + get_association(tx_type) => :optional + } + ) do + {:ok, batch_from_db} -> transactions_of_batch_changed?(batch_from_db, batch_from_rpc, tx_type) + {:error, :not_found} -> :error + end + + l1_tx = get_l1_tx_from_batch(batch_from_rpc, tx_type) + + if l1_tx.hash != Rpc.get_binary_zero_hash() and status_changed_or_error in [true, :error] do + l1_txs = Db.get_indices_for_l1_transactions(%{l1_tx.hash => l1_tx}) + + {:look_for_batches, l1_tx.hash, l1_txs} + else + {:skip, "", %{}} + end + end + + defp get_association(tx_type) do + case tx_type do + :commit_tx -> :commit_transaction + :prove_tx -> :prove_transaction + :execute_tx -> :execute_transaction + end + end + + defp transactions_of_batch_changed?(batch_db, batch_json, tx_type) do + tx_hash_json = + case tx_type do + :commit_tx -> batch_json.commit_tx_hash + :prove_tx -> batch_json.prove_tx_hash + :execute_tx -> batch_json.executed_tx_hash + end + + tx_hash_db = + case tx_type do + :commit_tx -> batch_db.commit_transaction + :prove_tx -> batch_db.prove_transaction + :execute_tx -> batch_db.execute_transaction + end + + tx_hash_db_bytes = + if is_nil(tx_hash_db) do + Rpc.get_binary_zero_hash() + else + tx_hash_db.hash.bytes + end + + tx_hash_json != tx_hash_db_bytes + end + + defp get_l1_tx_from_batch(batch_from_rpc, tx_type) do + case tx_type do + :commit_tx -> %{hash: batch_from_rpc.commit_tx_hash, timestamp: batch_from_rpc.commit_timestamp} + :prove_tx -> %{hash: batch_from_rpc.prove_tx_hash, timestamp: batch_from_rpc.prove_timestamp} + :execute_tx -> %{hash: batch_from_rpc.executed_tx_hash, timestamp: batch_from_rpc.executed_timestamp} + end + end + + @doc """ + Receives batches from the database, establishes an association between each batch and + the corresponding L1 transactions, and imports batches and L1 transactions into the database. + If the number of batches returned from the database does not match the requested batches, + the initial list of batch numbers is returned, assuming that they can be + used for the missed batch recovery procedure. + + ## Parameters + - `batches_numbers`: the list of batch numbers that must be updated. + - `l1_txs`: a map containing transaction hashes as keys, and values are maps + with transaction hashes and transaction timestamps of L1 transactions to import to the database. + - `tx_hash`: the hash of the L1 transaction to build an association with. + - `association_key`: the field in the batch description to build an association with L1 + transactions. + + ## Returns + - `:ok` if batches and the corresponding L1 transactions are imported successfully. + - `{:recovery_required, batches_to_recover}` if the absence of batches is discovered; + `batches_to_recover` contains the list of batch numbers. + """ + @spec associate_and_import_or_prepare_for_recovery([integer()], map(), binary(), :commit_id | :execute_id | :prove_id) :: + :ok | {:recovery_required, [integer()]} + def associate_and_import_or_prepare_for_recovery(batches_numbers, l1_txs, tx_hash, association_key) + when is_list(batches_numbers) and is_map(l1_txs) and is_binary(tx_hash) and + association_key in [:commit_id, :prove_id, :execute_id] do + case prepare_batches_to_import(batches_numbers, %{association_key => l1_txs[tx_hash][:id]}) do + {:error, batches_to_recover} -> + {:recovery_required, batches_to_recover} + + {:ok, batches_to_import} -> + Db.import_to_db(batches_to_import, Map.values(l1_txs)) + :ok + end + end + + # Receives batches from the database and merges each batch's data with the data provided + # in `map_to_update`. If the number of batches returned from the database does not match + # with the requested batches, the initial list of batch numbers is returned, assuming that they + # can be used for the missed batch recovery procedure. + # + # ## Parameters + # - `batches`: the list of batch numbers that must be updated. + # - `map_to_update`: a map containing new data that must be applied to all requested batches. + # + # ## Returns + # - `{:ok, batches_to_import}` where `batches_to_import` is the list of batches ready to import + # with updated data. + # - `{:error, batches}` where `batches` contains the input list of batch numbers. + defp prepare_batches_to_import(batches, map_to_update) do + batches_from_db = Reader.batches(batches, []) + + if length(batches_from_db) == length(batches) do + batches_to_import = + batches_from_db + |> Enum.reduce([], fn batch, batches -> + [ + batch + |> Rpc.transform_transaction_batch_to_map() + |> Map.merge(map_to_update) + | batches + ] + end) + + {:ok, batches_to_import} + else + log_warning("Lack of batches received from DB to update") + {:error, batches} + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/executed.ex b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/executed.ex new file mode 100644 index 000000000000..38d7db9d81a1 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/executed.ex @@ -0,0 +1,78 @@ +defmodule Indexer.Fetcher.ZkSync.StatusTracking.Executed do + @moduledoc """ + Functionality to discover executed batches + """ + + alias Indexer.Fetcher.ZkSync.Utils.{Db, Rpc} + + import Indexer.Fetcher.ZkSync.StatusTracking.CommonUtils, + only: [ + check_if_batch_status_changed: 3, + associate_and_import_or_prepare_for_recovery: 4 + ] + + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1] + + # keccak256("BlockExecution(uint256,bytes32,bytes32)") + @block_execution_event "0x2402307311a4d6604e4e7b4c8a15a7e1213edb39c16a31efa70afb06030d3165" + + @doc """ + Checks if the oldest unexecuted batch in the database has the associated L1 executing transaction + by requesting new batch details from RPC. If so, analyzes the `BlockExecution` event emitted by + the transaction to explore all the batches executed by it. For all discovered batches, it updates + the database with new associations, importing information about L1 transactions. + If it is found that some of the discovered batches are absent in the database, the function + interrupts and returns the list of batch numbers that can be attempted to be recovered. + + ## Parameters + - `config`: Configuration containing `json_l1_rpc_named_arguments` and + `json_l2_rpc_named_arguments` defining parameters for the RPC connections. + + ## Returns + - `:ok` if no new executed batches are found, or if all found batches and the corresponding L1 + transactions are imported successfully. + - `{:recovery_required, batches_to_recover}` if the absence of new executed batches is + discovered; `batches_to_recover` contains the list of batch numbers. + """ + @spec look_for_batches_and_update(%{ + :json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + :json_l2_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }) :: :ok | {:recovery_required, list()} + def look_for_batches_and_update( + %{ + json_l1_rpc_named_arguments: json_l1_rpc_named_arguments, + json_l2_rpc_named_arguments: json_l2_rpc_named_arguments + } = _config + ) do + case Db.get_earliest_unexecuted_batch_number() do + nil -> + :ok + + expected_batch_number -> + log_info("Checking if the batch #{expected_batch_number} was executed") + + {next_action, tx_hash, l1_txs} = + check_if_batch_status_changed(expected_batch_number, :execute_tx, json_l2_rpc_named_arguments) + + case next_action do + :skip -> + :ok + + :look_for_batches -> + log_info("The batch #{expected_batch_number} looks like executed") + execute_tx_receipt = Rpc.fetch_tx_receipt_by_hash(tx_hash, json_l1_rpc_named_arguments) + batches_numbers_from_rpc = get_executed_batches_from_logs(execute_tx_receipt["logs"]) + + associate_and_import_or_prepare_for_recovery(batches_numbers_from_rpc, l1_txs, tx_hash, :execute_id) + end + end + end + + defp get_executed_batches_from_logs(logs) do + executed_batches = Rpc.filter_logs_and_extract_topic_at(logs, @block_execution_event, 1) + log_info("Discovered #{length(executed_batches)} executed batches in the executing tx") + + executed_batches + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex new file mode 100644 index 000000000000..52165ef8f0eb --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex @@ -0,0 +1,137 @@ +defmodule Indexer.Fetcher.ZkSync.StatusTracking.Proven do + @moduledoc """ + Functionality to discover proven batches + """ + + alias ABI.{FunctionSelector, TypeDecoder} + alias Indexer.Fetcher.ZkSync.Utils.{Db, Rpc} + + import Indexer.Fetcher.ZkSync.StatusTracking.CommonUtils, + only: [ + check_if_batch_status_changed: 3, + associate_and_import_or_prepare_for_recovery: 4 + ] + + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1] + + @doc """ + Checks if the oldest unproven batch in the database has the associated L1 proving transaction + by requesting new batch details from RPC. If so, analyzes the calldata of the transaction + to explore all the batches proven by it. For all discovered batches, it updates + the database with new associations, importing information about L1 transactions. + If it is found that some of the discovered batches are absent in the database, the function + interrupts and returns the list of batch numbers that can be attempted to be recovered. + + ## Parameters + - `config`: Configuration containing `json_l1_rpc_named_arguments` and + `json_l2_rpc_named_arguments` defining parameters for the RPC connections. + + ## Returns + - `:ok` if no new proven batches are found, or if all found batches and the corresponding L1 + transactions are imported successfully. + - `{:recovery_required, batches_to_recover}` if the absence of new proven batches is + discovered; `batches_to_recover` contains the list of batch numbers. + """ + @spec look_for_batches_and_update(%{ + :json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + :json_l2_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }) :: :ok | {:recovery_required, list()} + def look_for_batches_and_update( + %{ + json_l1_rpc_named_arguments: json_l1_rpc_named_arguments, + json_l2_rpc_named_arguments: json_l2_rpc_named_arguments + } = _config + ) do + case Db.get_earliest_unproven_batch_number() do + nil -> + :ok + + expected_batch_number -> + log_info("Checking if the batch #{expected_batch_number} was proven") + + {next_action, tx_hash, l1_txs} = + check_if_batch_status_changed(expected_batch_number, :prove_tx, json_l2_rpc_named_arguments) + + case next_action do + :skip -> + :ok + + :look_for_batches -> + log_info("The batch #{expected_batch_number} looks like proven") + prove_tx = Rpc.fetch_tx_by_hash(tx_hash, json_l1_rpc_named_arguments) + batches_numbers_from_rpc = get_proven_batches_from_calldata(prove_tx["input"]) + + associate_and_import_or_prepare_for_recovery(batches_numbers_from_rpc, l1_txs, tx_hash, :prove_id) + end + end + end + + defp get_proven_batches_from_calldata(calldata) do + "0x7f61885c" <> encoded_params = calldata + + # /// @param batchNumber Rollup batch number + # /// @param batchHash Hash of L2 batch + # /// @param indexRepeatedStorageChanges The serial number of the shortcut index that's used as a unique identifier for storage keys that were used twice or more + # /// @param numberOfLayer1Txs Number of priority operations to be processed + # /// @param priorityOperationsHash Hash of all priority operations from this batch + # /// @param l2LogsTreeRoot Root hash of tree that contains L2 -> L1 messages from this batch + # /// @param timestamp Rollup batch timestamp, have the same format as Ethereum batch constant + # /// @param commitment Verified input for the zkSync circuit + # struct StoredBatchInfo { + # uint64 batchNumber; + # bytes32 batchHash; + # uint64 indexRepeatedStorageChanges; + # uint256 numberOfLayer1Txs; + # bytes32 priorityOperationsHash; + # bytes32 l2LogsTreeRoot; + # uint256 timestamp; + # bytes32 commitment; + # } + # /// @notice Recursive proof input data (individual commitments are constructed onchain) + # struct ProofInput { + # uint256[] recursiveAggregationInput; + # uint256[] serializedProof; + # } + # proveBatches(StoredBatchInfo calldata _prevBatch, StoredBatchInfo[] calldata _committedBatches, ProofInput calldata _proof) + + # IO.inspect(FunctionSelector.decode("proveBatches((uint64,bytes32,uint64,uint256,bytes32,bytes32,uint256,bytes32),(uint64,bytes32,uint64,uint256,bytes32,bytes32,uint256,bytes32)[],(uint256[],uint256[]))")) + [_prev_batch, proven_batches, _proof] = + TypeDecoder.decode( + Base.decode16!(encoded_params, case: :lower), + %FunctionSelector{ + function: "proveBatches", + types: [ + tuple: [ + uint: 64, + bytes: 32, + uint: 64, + uint: 256, + bytes: 32, + bytes: 32, + uint: 256, + bytes: 32 + ], + array: + {:tuple, + [ + uint: 64, + bytes: 32, + uint: 64, + uint: 256, + bytes: 32, + bytes: 32, + uint: 256, + bytes: 32 + ]}, + tuple: [array: {:uint, 256}, array: {:uint, 256}] + ] + } + ) + + log_info("Discovered #{length(proven_batches)} proven batches in the prove tx") + + proven_batches + |> Enum.map(fn batch_info -> elem(batch_info, 0) end) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/zksync/transaction_batch.ex new file mode 100644 index 000000000000..dac1b1d84304 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/transaction_batch.ex @@ -0,0 +1,149 @@ +defmodule Indexer.Fetcher.ZkSync.TransactionBatch do + @moduledoc """ + Discovers new batches and populates the `zksync_transaction_batches` table. + + Repetitiveness is supported by sending a `:continue` message to itself every `recheck_interval` seconds. + + Each iteration compares the number of the last handled batch stored in the state with the + latest batch available on the RPC node. If the rollup progresses, all batches between the + last handled batch (exclusively) and the latest available batch (inclusively) are downloaded from RPC + in chunks of `chunk_size` and imported into the `zksync_transaction_batches` table. If the latest + available batch is too far from the last handled batch, only `batches_max_range` batches are downloaded. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + alias Explorer.Chain.ZkSync.Reader + alias Indexer.Fetcher.ZkSync.Discovery.Workers + alias Indexer.Fetcher.ZkSync.Utils.Rpc + + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1] + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(args) do + Logger.metadata(fetcher: :zksync_transaction_batches) + + config = Application.get_all_env(:indexer)[Indexer.Fetcher.ZkSync.TransactionBatch] + chunk_size = config[:chunk_size] + recheck_interval = config[:recheck_interval] + batches_max_range = config[:batches_max_range] + + Process.send(self(), :init, []) + + {:ok, + %{ + config: %{ + chunk_size: chunk_size, + batches_max_range: batches_max_range, + json_rpc_named_arguments: args[:json_rpc_named_arguments], + recheck_interval: recheck_interval + }, + data: %{latest_handled_batch_number: 0} + }} + end + + @impl GenServer + def handle_info(:init, state) do + latest_handled_batch_number = + case Reader.latest_available_batch_number() do + nil -> + log_info("No batches found in DB. Will start with the latest batch available by RPC") + # The value received from RPC is decremented in order to not waste + # the first iteration of handling `:continue` message. + Rpc.fetch_latest_sealed_batch_number(state.config.json_rpc_named_arguments) - 1 + + latest_handled_batch_number -> + latest_handled_batch_number + end + + Process.send_after(self(), :continue, 2000) + + log_info("All batches including #{latest_handled_batch_number} are considered as handled") + + {:noreply, %{state | data: %{latest_handled_batch_number: latest_handled_batch_number}}} + end + + # Checks if the rollup progresses by comparing the recently stored batch + # with the latest batch received from RPC. If progress is detected, it downloads + # batches, builds their associations with rollup blocks and transactions, and + # imports the received data to the database. If the latest batch received from RPC + # is too far from the most recently stored batch, only `batches_max_range` batches + # are downloaded. All RPC calls to get batch details and receive transactions + # included in batches are made in chunks of `chunk_size`. + # + # After importing batch information, it schedules the next iteration by sending + # the `:continue` message. The sending of the message is delayed, taking into account + # the time remaining after downloading and importing processes. + # + # ## Parameters + # - `:continue`: The message triggering the handler. + # - `state`: The current state of the fetcher containing both the fetcher configuration + # and the latest handled batch number. + # + # ## Returns + # - `{:noreply, new_state}` where the latest handled batch number is updated with the largest + # of the batch numbers imported in the current iteration. + @impl GenServer + def handle_info( + :continue, + %{ + data: %{latest_handled_batch_number: latest_handled_batch_number}, + config: %{ + batches_max_range: batches_max_range, + json_rpc_named_arguments: json_rpc_named_arguments, + recheck_interval: recheck_interval, + chunk_size: _ + } + } = state + ) do + log_info("Checking for a new batch or batches") + + latest_sealed_batch_number = Rpc.fetch_latest_sealed_batch_number(json_rpc_named_arguments) + + {new_state, handle_duration} = + if latest_handled_batch_number < latest_sealed_batch_number do + start_batch_number = latest_handled_batch_number + 1 + end_batch_number = min(latest_sealed_batch_number, latest_handled_batch_number + batches_max_range) + + log_info("Handling the batch range #{start_batch_number}..#{end_batch_number}") + + {handle_duration, _} = + :timer.tc(&Workers.get_minimal_batches_info_and_import/3, [start_batch_number, end_batch_number, state.config]) + + { + %{state | data: %{latest_handled_batch_number: end_batch_number}}, + div(handle_duration, 1000) + } + else + {state, 0} + end + + Process.send_after(self(), :continue, max(:timer.seconds(recheck_interval) - handle_duration, 0)) + + {:noreply, new_state} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/utils/db.ex b/apps/indexer/lib/indexer/fetcher/zksync/utils/db.ex new file mode 100644 index 000000000000..12f7e51ba986 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/utils/db.ex @@ -0,0 +1,204 @@ +defmodule Indexer.Fetcher.ZkSync.Utils.Db do + @moduledoc """ + Common functions to simplify DB routines for Indexer.Fetcher.ZkSync fetchers + """ + + alias Explorer.Chain + alias Explorer.Chain.ZkSync.Reader + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_warning: 1, log_info: 1] + + @json_batch_fields_absent_in_db_batch [ + :commit_tx_hash, + :commit_timestamp, + :prove_tx_hash, + :prove_timestamp, + :executed_tx_hash, + :executed_timestamp + ] + + @doc """ + Deletes elements in the batch description map to prepare the batch for importing to + the database. + + ## Parameters + - `batch_with_json_fields`: a map describing a batch with elements that could remain + after downloading batch details from RPC. + + ## Returns + - A map describing the batch compatible with the database import operation. + """ + @spec prune_json_batch(map()) :: map() + def prune_json_batch(batch_with_json_fields) + when is_map(batch_with_json_fields) do + Map.drop(batch_with_json_fields, @json_batch_fields_absent_in_db_batch) + end + + @doc """ + Gets the oldest imported batch number. + + ## Parameters + - none + + ## Returns + - A batch number or `nil` if there are no batches in the database. + """ + @spec get_earliest_batch_number() :: nil | non_neg_integer() + def get_earliest_batch_number do + case Reader.oldest_available_batch_number() do + nil -> + log_warning("No batches found in DB") + nil + + value -> + value + end + end + + @doc """ + Gets the oldest imported batch number without an associated commitment L1 transaction. + + ## Parameters + - none + + ## Returns + - A batch number or `nil` in cases where there are no batches in the database or + all batches in the database are marked as committed. + """ + @spec get_earliest_sealed_batch_number() :: nil | non_neg_integer() + def get_earliest_sealed_batch_number do + case Reader.earliest_sealed_batch_number() do + nil -> + log_info("No uncommitted batches found in DB") + nil + + value -> + value + end + end + + @doc """ + Gets the oldest imported batch number without an associated proving L1 transaction. + + ## Parameters + - none + + ## Returns + - A batch number or `nil` in cases where there are no batches in the database or + all batches in the database are marked as proven. + """ + @spec get_earliest_unproven_batch_number() :: nil | non_neg_integer() + def get_earliest_unproven_batch_number do + case Reader.earliest_unproven_batch_number() do + nil -> + log_info("No unproven batches found in DB") + nil + + value -> + value + end + end + + @doc """ + Gets the oldest imported batch number without an associated executing L1 transaction. + + ## Parameters + - none + + ## Returns + - A batch number or `nil` in cases where there are no batches in the database or + all batches in the database are marked as executed. + """ + @spec get_earliest_unexecuted_batch_number() :: nil | non_neg_integer() + def get_earliest_unexecuted_batch_number do + case Reader.earliest_unexecuted_batch_number() do + nil -> + log_info("No not executed batches found in DB") + nil + + value -> + value + end + end + + @doc """ + Indexes L1 transactions provided in the input map. For transactions that + are already in the database, existing indices are taken. For new transactions, + the next available indices are assigned. + + ## Parameters + - `new_l1_txs`: A map of L1 transaction descriptions. The keys of the map are + transaction hashes. + + ## Returns + - `l1_txs`: A map of L1 transaction descriptions. Each element is extended with + the key `:id`, representing the index of the L1 transaction in the + `zksync_lifecycle_l1_transactions` table. + """ + @spec get_indices_for_l1_transactions(map()) :: any() + def get_indices_for_l1_transactions(new_l1_txs) + when is_map(new_l1_txs) do + # Get indices for l1 transactions previously handled + l1_txs = + new_l1_txs + |> Map.keys() + |> Reader.lifecycle_transactions() + |> Enum.reduce(new_l1_txs, fn {hash, id}, txs -> + {_, txs} = + Map.get_and_update!(txs, hash.bytes, fn l1_tx -> + {l1_tx, Map.put(l1_tx, :id, id)} + end) + + txs + end) + + # Get the next index for the first new transaction based + # on the indices existing in DB + l1_tx_next_id = Reader.next_id() + + # Assign new indices for the transactions which are not in + # the l1 transactions table yet + {updated_l1_txs, _} = + l1_txs + |> Map.keys() + |> Enum.reduce( + {l1_txs, l1_tx_next_id}, + fn hash, {txs, next_id} -> + tx = txs[hash] + id = Map.get(tx, :id) + + if is_nil(id) do + {Map.put(txs, hash, Map.put(tx, :id, next_id)), next_id + 1} + else + {txs, next_id} + end + end + ) + + updated_l1_txs + end + + @doc """ + Imports provided lists of batches and their associations with L1 transactions, rollup blocks, + and transactions to the database. + + ## Parameters + - `batches`: A list of maps with batch descriptions. + - `l1_txs`: A list of maps with L1 transaction descriptions. Optional. + - `l2_txs`: A list of maps with rollup transaction associations. Optional. + - `l2_blocks`: A list of maps with rollup block associations. Optional. + + ## Returns + n/a + """ + def import_to_db(batches, l1_txs \\ [], l2_txs \\ [], l2_blocks \\ []) + when is_list(batches) and is_list(l1_txs) and is_list(l2_txs) and is_list(l2_blocks) do + {:ok, _} = + Chain.import(%{ + zksync_lifecycle_transactions: %{params: l1_txs}, + zksync_transaction_batches: %{params: batches}, + zksync_batch_transactions: %{params: l2_txs}, + zksync_batch_blocks: %{params: l2_blocks}, + timeout: :infinity + }) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/utils/logging.ex b/apps/indexer/lib/indexer/fetcher/zksync/utils/logging.ex new file mode 100644 index 000000000000..eb7fe6058797 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/utils/logging.ex @@ -0,0 +1,143 @@ +defmodule Indexer.Fetcher.ZkSync.Utils.Logging do + @moduledoc """ + Common logging functions for Indexer.Fetcher.ZkSync fetchers + """ + require Logger + + @doc """ + A helper function to log a message with warning severity. Uses `Logger.warning` facility. + + ## Parameters + - `msg`: a message to log + + ## Returns + `:ok` + """ + @spec log_warning(any()) :: :ok + def log_warning(msg) do + Logger.warning(msg) + end + + @doc """ + A helper function to log a message with info severity. Uses `Logger.info` facility. + + ## Parameters + - `msg`: a message to log + + ## Returns + `:ok` + """ + @spec log_info(any()) :: :ok + def log_info(msg) do + Logger.info(msg) + end + + @doc """ + A helper function to log a message with error severity. Uses `Logger.error` facility. + + ## Parameters + - `msg`: a message to log + + ## Returns + `:ok` + """ + @spec log_error(any()) :: :ok + def log_error(msg) do + Logger.error(msg) + end + + @doc """ + A helper function to log progress when handling batches in chunks. + + ## Parameters + - `prefix`: A prefix for the logging message. + - `chunk`: A list of batch numbers in the current chunk. + - `current_progress`: The total number of batches handled up to this moment. + - `total`: The total number of batches across all chunks. + + ## Returns + `:ok` + + ## Examples: + - `log_details_chunk_handling("A message", [1, 2, 3], 0, 10)` produces + `A message for batches 1..3. Progress 30%` + - `log_details_chunk_handling("A message", [2], 1, 10)` produces + `A message for batch 2. Progress 20%` + - `log_details_chunk_handling("A message", [35], 0, 1)` produces + `A message for batch 35.` + - `log_details_chunk_handling("A message", [45, 50, 51, 52, 60], 1, 1)` produces + `A message for batches 45, 50..52, 60.` + """ + @spec log_details_chunk_handling(binary(), list(), non_neg_integer(), non_neg_integer()) :: :ok + def log_details_chunk_handling(prefix, chunk, current_progress, total) + when is_binary(prefix) and is_list(chunk) and (is_integer(current_progress) and current_progress >= 0) and + (is_integer(total) and total > 0) do + chunk_length = length(chunk) + + progress = + case chunk_length == total do + true -> + "" + + false -> + percentage = + (current_progress + chunk_length) + |> Decimal.div(total) + |> Decimal.mult(100) + |> Decimal.round(2) + |> Decimal.to_string() + + " Progress: #{percentage}%" + end + + if chunk_length == 1 do + log_info("#{prefix} for batch ##{Enum.at(chunk, 0)}.") + else + log_info("#{prefix} for batches #{Enum.join(shorten_numbers_list(chunk), ", ")}.#{progress}") + end + end + + # Transform list of numbers to the list of string where consequent values + # are combined to be displayed as a range. + # + # ## Parameters + # - `msg`: a message to log + # + # ## Returns + # `shorten_list` - resulting list after folding + # + # ## Examples: + # [1, 2, 3] => ["1..3"] + # [1, 3] => ["1", "3"] + # [1, 2] => ["1..2"] + # [1, 3, 4, 5] => ["1", "3..5"] + defp shorten_numbers_list(numbers_list) do + {shorten_list, _, _} = + numbers_list + |> Enum.sort() + |> Enum.reduce({[], nil, nil}, fn number, {shorten_list, prev_range_start, prev_number} -> + shorten_numbers_list_impl(number, shorten_list, prev_range_start, prev_number) + end) + |> then(fn {shorten_list, prev_range_start, prev_number} -> + shorten_numbers_list_impl(prev_number, shorten_list, prev_range_start, prev_number) + end) + + Enum.reverse(shorten_list) + end + + defp shorten_numbers_list_impl(number, shorten_list, prev_range_start, prev_number) do + cond do + is_nil(prev_number) -> + {[], number, number} + + prev_number + 1 != number and prev_range_start == prev_number -> + {["#{prev_range_start}" | shorten_list], number, number} + + prev_number + 1 != number -> + {["#{prev_range_start}..#{prev_number}" | shorten_list], number, number} + + true -> + {shorten_list, prev_range_start, number} + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex b/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex new file mode 100644 index 000000000000..282d60b35146 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex @@ -0,0 +1,403 @@ +defmodule Indexer.Fetcher.ZkSync.Utils.Rpc do + @moduledoc """ + Common functions to handle RPC calls for Indexer.Fetcher.ZkSync fetchers + """ + + import EthereumJSONRPC, only: [json_rpc: 2, quantity_to_integer: 1] + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_error: 1] + + @zero_hash "0000000000000000000000000000000000000000000000000000000000000000" + @zero_hash_binary <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> + + @rpc_resend_attempts 20 + + def get_zero_hash do + @zero_hash + end + + def get_binary_zero_hash do + @zero_hash_binary + end + + @doc """ + Filters out logs from a list of transactions logs where topic #0 is `topic_0` and + builds a list of values located at position `position` in such logs. + + ## Parameters + - `logs`: The list of transaction logs to filter logs with a specific topic. + - `topic_0`: The value of topic #0 in the required logs. + - `position`: The topic number to be extracted from the topic lists of every log + and appended to the resulting list. + + ## Returns + - A list of values extracted from the required transaction logs. + - An empty list if no logs with the specified topic are found. + """ + @spec filter_logs_and_extract_topic_at(maybe_improper_list(), binary(), integer()) :: list() + def filter_logs_and_extract_topic_at(logs, topic_0, position) + when is_list(logs) and + is_binary(topic_0) and + (is_integer(position) and position >= 0 and position <= 3) do + logs + |> Enum.reduce([], fn log_entity, result -> + topics = log_entity["topics"] + + if Enum.at(topics, 0) == topic_0 do + [quantity_to_integer(Enum.at(topics, position)) | result] + else + result + end + end) + end + + defp from_ts_to_datetime(time_ts) do + {_, unix_epoch_starts} = DateTime.from_unix(0) + + case is_nil(time_ts) or time_ts == 0 do + true -> + unix_epoch_starts + + false -> + case DateTime.from_unix(time_ts) do + {:ok, datetime} -> + datetime + + {:error, _} -> + unix_epoch_starts + end + end + end + + defp from_iso8601_to_datetime(time_string) do + case is_nil(time_string) do + true -> + from_ts_to_datetime(0) + + false -> + case DateTime.from_iso8601(time_string) do + {:ok, datetime, _} -> + datetime + + {:error, _} -> + from_ts_to_datetime(0) + end + end + end + + defp json_txid_to_hash(hash) do + case hash do + "0x" <> tx_hash -> tx_hash + nil -> @zero_hash + end + end + + defp strhash_to_byteshash(hash) do + hash + |> json_txid_to_hash() + |> Base.decode16!(case: :mixed) + end + + @doc """ + Transforms a map with batch data received from the `zks_getL1BatchDetails` call + into a map that can be used by Indexer.Fetcher.ZkSync fetchers for further handling. + All hexadecimal hashes are converted to their decoded binary representation, + Unix and ISO8601 timestamps are converted to DateTime objects. + + ## Parameters + - `json_response`: Raw data received from the JSON RPC call. + + ## Returns + - A map containing minimal information about the batch. `start_block` and `end_block` + elements are set to `nil`. + """ + @spec transform_batch_details_to_map(map()) :: map() + def transform_batch_details_to_map(json_response) + when is_map(json_response) do + %{ + "number" => {:number, :ok}, + "timestamp" => {:timestamp, :ts_to_datetime}, + "l1TxCount" => {:l1_tx_count, :ok}, + "l2TxCount" => {:l2_tx_count, :ok}, + "rootHash" => {:root_hash, :str_to_byteshash}, + "commitTxHash" => {:commit_tx_hash, :str_to_byteshash}, + "committedAt" => {:commit_timestamp, :iso8601_to_datetime}, + "proveTxHash" => {:prove_tx_hash, :str_to_byteshash}, + "provenAt" => {:prove_timestamp, :iso8601_to_datetime}, + "executeTxHash" => {:executed_tx_hash, :str_to_byteshash}, + "executedAt" => {:executed_timestamp, :iso8601_to_datetime}, + "l1GasPrice" => {:l1_gas_price, :ok}, + "l2FairGasPrice" => {:l2_fair_gas_price, :ok} + # :start_block added by request_block_ranges_by_rpc + # :end_block added by request_block_ranges_by_rpc + } + |> Enum.reduce(%{start_block: nil, end_block: nil}, fn {key, {key_atom, transform_type}}, batch_details_map -> + value_in_json_response = Map.get(json_response, key) + + Map.put( + batch_details_map, + key_atom, + case transform_type do + :iso8601_to_datetime -> from_iso8601_to_datetime(value_in_json_response) + :ts_to_datetime -> from_ts_to_datetime(value_in_json_response) + :str_to_txhash -> json_txid_to_hash(value_in_json_response) + :str_to_byteshash -> strhash_to_byteshash(value_in_json_response) + _ -> value_in_json_response + end + ) + end) + end + + @doc """ + Transforms a map with batch data received from the database into a map that + can be used by Indexer.Fetcher.ZkSync fetchers for further handling. + + ## Parameters + - `batch`: A map containing a batch description received from the database. + + ## Returns + - A map containing simplified representation of the batch. Compatible with + the database import operation. + """ + def transform_transaction_batch_to_map(batch) + when is_map(batch) do + %{ + number: batch.number, + timestamp: batch.timestamp, + l1_tx_count: batch.l1_tx_count, + l2_tx_count: batch.l2_tx_count, + root_hash: batch.root_hash.bytes, + l1_gas_price: batch.l1_gas_price, + l2_fair_gas_price: batch.l2_fair_gas_price, + start_block: batch.start_block, + end_block: batch.end_block, + commit_id: batch.commit_id, + prove_id: batch.prove_id, + execute_id: batch.execute_id + } + end + + @doc """ + Retrieves batch details from the RPC endpoint using the `zks_getL1BatchDetails` call. + + ## Parameters + - `batch_number`: The batch number or identifier. + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A map containing minimal batch details. It includes `start_block` and `end_block` + elements, both set to `nil`. + """ + @spec fetch_batch_details_by_batch_number(binary() | non_neg_integer(), EthereumJSONRPC.json_rpc_named_arguments()) :: + map() + def fetch_batch_details_by_batch_number(batch_number, json_rpc_named_arguments) + when (is_integer(batch_number) or is_binary(batch_number)) and is_list(json_rpc_named_arguments) do + req = + EthereumJSONRPC.request(%{ + id: batch_number, + method: "zks_getL1BatchDetails", + params: [batch_number] + }) + + error_message = &"Cannot call zks_getL1BatchDetails. Error: #{inspect(&1)}" + + {:ok, resp} = repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + transform_batch_details_to_map(resp) + end + + @doc """ + Fetches transaction details from the RPC endpoint using the `eth_getTransactionByHash` call. + + ## Parameters + - `raw_hash`: The hash of the Ethereum transaction. It can be provided as a decoded binary + or hexadecimal string. + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A map containing details of the transaction. + """ + @spec fetch_tx_by_hash(binary(), EthereumJSONRPC.json_rpc_named_arguments()) :: map() + def fetch_tx_by_hash(raw_hash, json_rpc_named_arguments) + when is_binary(raw_hash) and is_list(json_rpc_named_arguments) do + hash = + case raw_hash do + "0x" <> _ -> raw_hash + _ -> "0x" <> Base.encode16(raw_hash) + end + + req = + EthereumJSONRPC.request(%{ + id: 0, + method: "eth_getTransactionByHash", + params: [hash] + }) + + error_message = &"Cannot call eth_getTransactionByHash for hash #{hash}. Error: #{inspect(&1)}" + + {:ok, resp} = repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + resp + end + + @doc """ + Fetches the transaction receipt from the RPC endpoint using the `eth_getTransactionReceipt` call. + + ## Parameters + - `raw_hash`: The hash of the Ethereum transaction. It can be provided as a decoded binary + or hexadecimal string. + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A map containing the receipt details of the transaction. + """ + @spec fetch_tx_receipt_by_hash(binary(), EthereumJSONRPC.json_rpc_named_arguments()) :: map() + def fetch_tx_receipt_by_hash(raw_hash, json_rpc_named_arguments) + when is_binary(raw_hash) and is_list(json_rpc_named_arguments) do + hash = + case raw_hash do + "0x" <> _ -> raw_hash + _ -> "0x" <> Base.encode16(raw_hash) + end + + req = + EthereumJSONRPC.request(%{ + id: 0, + method: "eth_getTransactionReceipt", + params: [hash] + }) + + error_message = &"Cannot call eth_getTransactionReceipt for hash #{hash}. Error: #{inspect(&1)}" + + {:ok, resp} = repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + resp + end + + @doc """ + Fetches the latest sealed batch number from the RPC endpoint using the `zks_L1BatchNumber` call. + + ## Parameters + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A non-negative integer representing the latest sealed batch number. + """ + @spec fetch_latest_sealed_batch_number(EthereumJSONRPC.json_rpc_named_arguments()) :: nil | non_neg_integer() + def fetch_latest_sealed_batch_number(json_rpc_named_arguments) + when is_list(json_rpc_named_arguments) do + req = EthereumJSONRPC.request(%{id: 0, method: "zks_L1BatchNumber", params: []}) + + error_message = &"Cannot call zks_L1BatchNumber. Error: #{inspect(&1)}" + + {:ok, resp} = repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + quantity_to_integer(resp) + end + + @doc """ + Fetches block details using multiple `eth_getBlockByNumber` RPC calls. + + ## Parameters + - `requests_list`: A list of `EthereumJSONRPC.Transport.request()` representing multiple + `eth_getBlockByNumber` RPC calls for different block numbers. + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A list of responses containing details of the requested blocks. + """ + @spec fetch_blocks_details([EthereumJSONRPC.Transport.request()], EthereumJSONRPC.json_rpc_named_arguments()) :: + list() + def fetch_blocks_details(requests_list, json_rpc_named_arguments) + + def fetch_blocks_details([], _) do + [] + end + + def fetch_blocks_details(requests_list, json_rpc_named_arguments) + when is_list(requests_list) and is_list(json_rpc_named_arguments) do + error_message = &"Cannot call eth_getBlockByNumber. Error: #{inspect(&1)}" + + {:ok, responses} = + repeated_call(&json_rpc/2, [requests_list, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + responses + end + + @doc """ + Fetches batches details using multiple `zks_getL1BatchDetails` RPC calls. + + ## Parameters + - `requests_list`: A list of `EthereumJSONRPC.Transport.request()` representing multiple + `zks_getL1BatchDetails` RPC calls for different block numbers. + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A list of responses containing details of the requested batches. + """ + @spec fetch_batches_details([EthereumJSONRPC.Transport.request()], EthereumJSONRPC.json_rpc_named_arguments()) :: + list() + def fetch_batches_details(requests_list, json_rpc_named_arguments) + + def fetch_batches_details([], _) do + [] + end + + def fetch_batches_details(requests_list, json_rpc_named_arguments) + when is_list(requests_list) and is_list(json_rpc_named_arguments) do + error_message = &"Cannot call zks_getL1BatchDetails. Error: #{inspect(&1)}" + + {:ok, responses} = + repeated_call(&json_rpc/2, [requests_list, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + responses + end + + @doc """ + Fetches block ranges included in the specified batches by using multiple + `zks_getL1BatchBlockRange` RPC calls. + + ## Parameters + - `requests_list`: A list of `EthereumJSONRPC.Transport.request()` representing multiple + `zks_getL1BatchBlockRange` RPC calls for different batch numbers. + - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + + ## Returns + - A list of responses containing block ranges associated with the requested batches. + """ + @spec fetch_blocks_ranges([EthereumJSONRPC.Transport.request()], EthereumJSONRPC.json_rpc_named_arguments()) :: + list() + def fetch_blocks_ranges(requests_list, json_rpc_named_arguments) + + def fetch_blocks_ranges([], _) do + [] + end + + def fetch_blocks_ranges(requests_list, json_rpc_named_arguments) + when is_list(requests_list) and is_list(json_rpc_named_arguments) do + error_message = &"Cannot call zks_getL1BatchBlockRange. Error: #{inspect(&1)}" + + {:ok, responses} = + repeated_call(&json_rpc/2, [requests_list, json_rpc_named_arguments], error_message, @rpc_resend_attempts) + + responses + end + + defp repeated_call(func, args, error_message, retries_left) do + case apply(func, args) do + {:ok, _} = res -> + res + + {:error, message} = err -> + retries_left = retries_left - 1 + + if retries_left <= 0 do + log_error(error_message.(message)) + err + else + log_error("#{error_message.(message)} Retrying...") + :timer.sleep(3000) + repeated_call(func, args, error_message, retries_left) + end + end + end +end diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex index 540606fe222f..08552d626795 100644 --- a/apps/indexer/lib/indexer/helper.ex +++ b/apps/indexer/lib/indexer/helper.ex @@ -3,8 +3,44 @@ defmodule Indexer.Helper do Auxiliary common functions for indexers. """ + require Logger + + import EthereumJSONRPC, + only: [ + fetch_block_number_by_tag: 2, + json_rpc: 2, + quantity_to_integer: 1, + request: 1 + ] + + alias EthereumJSONRPC.Block.ByNumber + alias EthereumJSONRPC.Blocks alias Explorer.Chain.Hash + @finite_retries_number 3 + @infinite_retries_number 100_000_000 + @block_check_interval_range_size 100 + @block_by_number_chunk_size 50 + + @doc """ + Checks whether the given Ethereum address looks correct. + The address should begin with 0x prefix and then contain 40 hexadecimal digits (can be in mixed case). + This function doesn't check if the address is checksummed. + """ + @spec address_correct?(binary()) :: boolean() + def address_correct?(address) when is_binary(address) do + String.match?(address, ~r/^0x[[:xdigit:]]{40}$/i) + end + + def address_correct?(_address) do + false + end + + @doc """ + Converts Explorer.Chain.Hash representation of the given address to a string + beginning with 0x prefix. If the given address is already a string, it is not modified. + The second argument forces the result to be downcased. + """ @spec address_hash_to_string(binary(), boolean()) :: binary() def address_hash_to_string(hash, downcase \\ false) @@ -24,12 +60,251 @@ defmodule Indexer.Helper do end end - @spec is_address_correct?(binary()) :: boolean() - def is_address_correct?(address) when is_binary(address) do - String.match?(address, ~r/^0x[[:xdigit:]]{40}$/i) + @doc """ + Calculates average block time in milliseconds (based on the latest 100 blocks) divided by 2. + Sends corresponding requests to the RPC node. + Returns a tuple {:ok, block_check_interval, last_safe_block} + where `last_safe_block` is the number of the recent `safe` or `latest` block (depending on which one is available). + Returns {:error, description} in case of error. + """ + @spec get_block_check_interval(list()) :: {:ok, non_neg_integer(), non_neg_integer()} | {:error, any()} + def get_block_check_interval(json_rpc_named_arguments) do + {last_safe_block, _} = get_safe_block(json_rpc_named_arguments) + + first_block = max(last_safe_block - @block_check_interval_range_size, 1) + + with {:ok, first_block_timestamp} <- + get_block_timestamp_by_number(first_block, json_rpc_named_arguments, 100_000_000), + {:ok, last_safe_block_timestamp} <- + get_block_timestamp_by_number(last_safe_block, json_rpc_named_arguments, 100_000_000) do + block_check_interval = + ceil((last_safe_block_timestamp - first_block_timestamp) / (last_safe_block - first_block) * 1000 / 2) + + Logger.info("Block check interval is calculated as #{block_check_interval} ms.") + {:ok, block_check_interval, last_safe_block} + else + {:error, error} -> + {:error, "Failed to calculate block check interval due to #{inspect(error)}"} + end end - def is_address_correct?(_address) do - false + defp get_safe_block(json_rpc_named_arguments) do + case get_block_number_by_tag("safe", json_rpc_named_arguments) do + {:ok, safe_block} -> + {safe_block, false} + + {:error, :not_found} -> + {:ok, latest_block} = get_block_number_by_tag("latest", json_rpc_named_arguments, 100_000_000) + {latest_block, true} + end + end + + @doc """ + Fetches block number by its tag (e.g. `latest` or `safe`) using RPC request. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_block_number_by_tag(binary(), list(), non_neg_integer()) :: {:ok, non_neg_integer()} | {:error, atom()} + def get_block_number_by_tag(tag, json_rpc_named_arguments, retries \\ @finite_retries_number) do + error_message = &"Cannot fetch #{tag} block number. Error: #{inspect(&1)}" + repeated_call(&fetch_block_number_by_tag/2, [tag, json_rpc_named_arguments], error_message, retries) + end + + @doc """ + Fetches transaction data by its hash using RPC request. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_transaction_by_hash(binary() | nil, list(), non_neg_integer()) :: {:ok, any()} | {:error, any()} + def get_transaction_by_hash(hash, json_rpc_named_arguments, retries_left \\ @finite_retries_number) + + def get_transaction_by_hash(hash, _json_rpc_named_arguments, _retries_left) when is_nil(hash), do: {:ok, nil} + + def get_transaction_by_hash(hash, json_rpc_named_arguments, retries) do + req = + request(%{ + id: 0, + method: "eth_getTransactionByHash", + params: [hash] + }) + + error_message = &"eth_getTransactionByHash failed. Error: #{inspect(&1)}" + + repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) + end + + def infinite_retries_number do + @infinite_retries_number + end + + @doc """ + Forms JSON RPC named arguments for the given RPC URL. + """ + @spec json_rpc_named_arguments(binary()) :: list() + def json_rpc_named_arguments(rpc_url) do + [ + transport: EthereumJSONRPC.HTTP, + transport_options: [ + http: EthereumJSONRPC.HTTP.HTTPoison, + url: rpc_url, + http_options: [ + recv_timeout: :timer.minutes(10), + timeout: :timer.minutes(10), + hackney: [pool: :ethereum_jsonrpc] + ] + ] + ] + end + + @doc """ + Prints a log of progress when handling something splitted to block chunks. + """ + @spec log_blocks_chunk_handling( + non_neg_integer(), + non_neg_integer(), + non_neg_integer(), + non_neg_integer(), + binary() | nil, + :L1 | :L2 + ) :: :ok + def log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, items_count, layer) do + is_start = is_nil(items_count) + + {type, found} = + if is_start do + {"Start", ""} + else + {"Finish", " Found #{items_count}."} + end + + target_range = + if chunk_start != start_block or chunk_end != end_block do + progress = + if is_start do + "" + else + percentage = + (chunk_end - start_block + 1) + |> Decimal.div(end_block - start_block + 1) + |> Decimal.mult(100) + |> Decimal.round(2) + |> Decimal.to_string() + + " Progress: #{percentage}%" + end + + " Target range: #{start_block}..#{end_block}.#{progress}" + else + "" + end + + if chunk_start == chunk_end do + Logger.info("#{type} handling #{layer} block ##{chunk_start}.#{found}#{target_range}") + else + Logger.info("#{type} handling #{layer} block range #{chunk_start}..#{chunk_end}.#{found}#{target_range}") + end + end + + @doc """ + Calls the given function with the given arguments + until it returns {:ok, any()} or the given attempts number is reached. + Pauses execution between invokes for 3..1200 seconds (depending on the number of retries). + """ + @spec repeated_call((... -> any()), list(), (... -> any()), non_neg_integer()) :: + {:ok, any()} | {:error, binary() | atom() | map()} + def repeated_call(func, args, error_message, retries_left, retries_done \\ 0) do + case apply(func, args) do + {:ok, _} = res -> + res + + {:error, message} = err -> + retries_left = retries_left - 1 + + if retries_left <= 0 do + Logger.error(error_message.(message)) + err + else + Logger.error("#{error_message.(message)} Retrying...") + + # wait up to 20 minutes + :timer.sleep(min(3000 * Integer.pow(2, retries_done), 1_200_000)) + + repeated_call(func, args, error_message, retries_left, retries_done + 1) + end + end + end + + @doc """ + Fetches blocks info from the given list of events (logs). + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_blocks_by_events(list(), list(), non_neg_integer()) :: list() + def get_blocks_by_events(events, json_rpc_named_arguments, retries) do + events + |> Enum.reduce(%{}, fn event, acc -> + block_number = Map.get(event, :block_number, event["blockNumber"]) + Map.put(acc, block_number, 0) + end) + |> Stream.map(fn {block_number, _} -> %{number: block_number} end) + |> Stream.with_index() + |> Enum.into(%{}, fn {params, id} -> {id, params} end) + |> Blocks.requests(&ByNumber.request(&1, false, false)) + |> Enum.chunk_every(@block_by_number_chunk_size) + |> Enum.reduce([], fn current_requests, results_acc -> + error_message = + &"Cannot fetch blocks with batch request. Error: #{inspect(&1)}. Request: #{inspect(current_requests)}" + + # credo:disable-for-lines:3 Credo.Check.Refactor.Nesting + results = + case repeated_call(&json_rpc/2, [current_requests, json_rpc_named_arguments], error_message, retries) do + {:ok, results} -> Enum.map(results, fn %{result: result} -> result end) + {:error, _} -> [] + end + + results_acc ++ results + end) + end + + @doc """ + Fetches block timestamp by its number using RPC request. + Performs a specified number of retries (up to) if the first attempt returns error. + """ + @spec get_block_timestamp_by_number(non_neg_integer(), list(), non_neg_integer()) :: + {:ok, non_neg_integer()} | {:error, any()} + def get_block_timestamp_by_number(number, json_rpc_named_arguments, retries \\ @finite_retries_number) do + func = &get_block_timestamp_by_number_inner/2 + args = [number, json_rpc_named_arguments] + error_message = &"Cannot fetch block ##{number} or its timestamp. Error: #{inspect(&1)}" + repeated_call(func, args, error_message, retries) + end + + defp get_block_timestamp_by_number_inner(number, json_rpc_named_arguments) do + result = + %{id: 0, number: number} + |> ByNumber.request(false) + |> json_rpc(json_rpc_named_arguments) + + with {:ok, block} <- result, + false <- is_nil(block), + timestamp <- Map.get(block, "timestamp"), + false <- is_nil(timestamp) do + {:ok, quantity_to_integer(timestamp)} + else + {:error, message} -> + {:error, message} + + true -> + {:error, "RPC returned nil."} + end + end + + @doc """ + Converts a log topic from Hash.Full representation to string one. + """ + @spec log_topic_to_string(any()) :: binary() | nil + def log_topic_to_string(topic) do + if is_binary(topic) or is_nil(topic) do + topic + else + Hash.to_string(topic) + end end end diff --git a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex index 6e6053e7b35a..17de593bc01a 100644 --- a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex +++ b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex @@ -13,7 +13,6 @@ defmodule Indexer.PendingTransactionsSanitizer do alias Ecto.Changeset alias Explorer.{Chain, Repo} - alias Explorer.Chain.Hash.Full, as: Hash alias Explorer.Chain.Import.Runner.Blocks alias Explorer.Chain.Transaction @@ -138,13 +137,13 @@ defmodule Indexer.PendingTransactionsSanitizer do defp fetch_block_and_invalidate(block_hash, pending_tx, tx) do case Chain.fetch_block_by_hash(block_hash) do - %{number: number, consensus: consensus} -> + %{number: number, consensus: consensus} = block -> Logger.debug( "Corresponding number of the block with hash #{block_hash} to invalidate is #{number} and consensus #{consensus}", fetcher: :pending_transactions_to_refetch ) - invalidate_block(number, block_hash, consensus, pending_tx, tx) + invalidate_block(block, pending_tx, tx) _ -> Logger.debug( @@ -154,11 +153,10 @@ defmodule Indexer.PendingTransactionsSanitizer do end end - defp invalidate_block(block_number, block_hash, consensus, pending_tx, tx) do - if consensus do - Blocks.invalidate_consensus_blocks([block_number]) + defp invalidate_block(block, pending_tx, tx) do + if block.consensus do + Blocks.invalidate_consensus_blocks([block.number]) else - {:ok, hash} = Hash.cast(block_hash) tx_info = to_elixir(tx) changeset = @@ -167,13 +165,15 @@ defmodule Indexer.PendingTransactionsSanitizer do |> Changeset.put_change(:cumulative_gas_used, tx_info["cumulativeGasUsed"]) |> Changeset.put_change(:gas_used, tx_info["gasUsed"]) |> Changeset.put_change(:index, tx_info["transactionIndex"]) - |> Changeset.put_change(:block_number, block_number) - |> Changeset.put_change(:block_hash, hash) + |> Changeset.put_change(:block_number, block.number) + |> Changeset.put_change(:block_hash, block.hash) + |> Changeset.put_change(:block_timestamp, block.timestamp) + |> Changeset.put_change(:block_consensus, false) Repo.update(changeset) Logger.debug( - "Pending tx with hash #{"0x" <> Base.encode16(pending_tx.hash.bytes, case: :lower)} assigned to block ##{block_number} with hash #{block_hash}" + "Pending tx with hash #{"0x" <> Base.encode16(pending_tx.hash.bytes, case: :lower)} assigned to block ##{block.number} with hash #{block.hash}" ) end end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 5fce739019ca..7ddb8be98b2b 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -5,29 +5,38 @@ defmodule Indexer.Supervisor do use Supervisor + alias Explorer.Chain.BridgedToken + alias Indexer.{ Block, + BridgedTokens.CalcLpTokensTotalLiquidity, + BridgedTokens.SetAmbBridgedMetadataForTokens, + BridgedTokens.SetOmniBridgedMetadataForTokens, PendingOpsCleaner, PendingTransactionsSanitizer } alias Indexer.Block.Catchup, as: BlockCatchup alias Indexer.Block.Realtime, as: BlockRealtime + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime + alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.TokenInstance.LegacySanitize, as: TokenInstanceLegacySanitize alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry alias Indexer.Fetcher.TokenInstance.Sanitize, as: TokenInstanceSanitize + alias Indexer.Fetcher.TokenInstance.SanitizeERC1155, as: TokenInstanceSanitizeERC1155 + alias Indexer.Fetcher.TokenInstance.SanitizeERC721, as: TokenInstanceSanitizeERC721 alias Indexer.Fetcher.{ BlockReward, - CoinBalance, ContractCode, EmptyBlocksSanitizer, InternalTransaction, PendingBlockOperationsSanitizer, PendingTransaction, - PolygonEdge, ReplacedTransaction, + RootstockData, Token, TokenBalance, TokenTotalSupplyUpdater, @@ -37,7 +46,8 @@ defmodule Indexer.Supervisor do Withdrawal } - alias Indexer.Fetcher.Zkevm.TransactionBatch + alias Indexer.Fetcher.ZkSync.BatchesStatusTracker, as: ZkSyncBatchesStatusTracker + alias Indexer.Fetcher.ZkSync.TransactionBatch, as: ZkSyncTransactionBatch alias Indexer.Temporary.{ BlocksTransactionsMismatch, @@ -112,13 +122,17 @@ defmodule Indexer.Supervisor do [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {InternalTransaction.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, - {CoinBalance.Supervisor, + {CoinBalanceCatchup.Supervisor, + [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, + {CoinBalanceRealtime.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {Token.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {TokenInstanceRealtime.Supervisor, [[memory_monitor: memory_monitor]]}, {TokenInstanceRetry.Supervisor, [[memory_monitor: memory_monitor]]}, {TokenInstanceSanitize.Supervisor, [[memory_monitor: memory_monitor]]}, - {TokenInstanceLegacySanitize.Supervisor, [[memory_monitor: memory_monitor]]}, + configure(TokenInstanceLegacySanitize, [[memory_monitor: memory_monitor]]), + configure(TokenInstanceSanitizeERC721, [[memory_monitor: memory_monitor]]), + configure(TokenInstanceSanitizeERC1155, [[memory_monitor: memory_monitor]]), configure(TransactionAction.Supervisor, [[memory_monitor: memory_monitor]]), {ContractCode.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, @@ -127,16 +141,45 @@ defmodule Indexer.Supervisor do {TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, - {PolygonEdge.Supervisor, [[memory_monitor: memory_monitor]]}, - {Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, [[memory_monitor: memory_monitor]]}, - {Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, - [[memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments]]}, - {Indexer.Fetcher.PolygonEdge.Withdrawal.Supervisor, - [[memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments]]}, - {Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, [[memory_monitor: memory_monitor]]}, - configure(TransactionBatch.Supervisor, [ + {Indexer.Fetcher.RollupL1ReorgMonitor.Supervisor, [[memory_monitor: memory_monitor]]}, + configure( + Indexer.Fetcher.Optimism.TxnBatch.Supervisor, + [[memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments]] + ), + configure(Indexer.Fetcher.Optimism.OutputRoot.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.Optimism.Deposit.Supervisor, [[memory_monitor: memory_monitor]]), + configure( + Indexer.Fetcher.Optimism.Withdrawal.Supervisor, + [[memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments]] + ), + configure(Indexer.Fetcher.Optimism.WithdrawalEvent.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, [ + [memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments] + ]), + configure(Indexer.Fetcher.PolygonEdge.Withdrawal.Supervisor, [ + [memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments] + ]), + configure(Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.Shibarium.L2.Supervisor, [ + [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] + ]), + configure(Indexer.Fetcher.Shibarium.L1.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.PolygonZkevm.BridgeL1.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens.Supervisor, [[memory_monitor: memory_monitor]]), + configure(Indexer.Fetcher.PolygonZkevm.BridgeL2.Supervisor, [ + [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] + ]), + configure(ZkSyncTransactionBatch.Supervisor, [ + [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] + ]), + configure(ZkSyncBatchesStatusTracker.Supervisor, [ + [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] + ]), + configure(Indexer.Fetcher.PolygonZkevm.TransactionBatch.Supervisor, [ [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] ]), + {Indexer.Fetcher.Beacon.Blob.Supervisor, [[memory_monitor: memory_monitor]]}, # Out-of-band fetchers {EmptyBlocksSanitizer.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, @@ -151,27 +194,62 @@ defmodule Indexer.Supervisor do [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {PendingOpsCleaner, [[], []]}, {PendingBlockOperationsSanitizer, [[]]}, + {RootstockData.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, # Block fetchers configure(BlockRealtime.Supervisor, [ %{block_fetcher: realtime_block_fetcher, subscribe_named_arguments: realtime_subscribe_named_arguments}, [name: BlockRealtime.Supervisor] ]), - {BlockCatchup.Supervisor, - [ - %{block_fetcher: block_fetcher, block_interval: block_interval, memory_monitor: memory_monitor}, - [name: BlockCatchup.Supervisor] - ]}, + configure( + BlockCatchup.Supervisor, + [ + %{block_fetcher: block_fetcher, block_interval: block_interval, memory_monitor: memory_monitor}, + [name: BlockCatchup.Supervisor] + ] + ), {Withdrawal.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]} ] |> List.flatten() + all_fetchers = + basic_fetchers + |> maybe_add_bridged_tokens_fetchers() + |> add_chain_type_dependent_fetchers() + Supervisor.init( - basic_fetchers, + all_fetchers, strategy: :one_for_one ) end + defp maybe_add_bridged_tokens_fetchers(basic_fetchers) do + extended_fetchers = + if BridgedToken.enabled?() && BridgedToken.necessary_envs_passed?() do + [{CalcLpTokensTotalLiquidity, [[], []]}, {SetOmniBridgedMetadataForTokens, [[], []]}] ++ basic_fetchers + else + basic_fetchers + end + + amb_bridge_mediators = Application.get_env(:explorer, Explorer.Chain.BridgedToken)[:amb_bridge_mediators] + + if BridgedToken.enabled?() && amb_bridge_mediators && amb_bridge_mediators !== "" do + [{SetAmbBridgedMetadataForTokens, [[], []]} | extended_fetchers] + else + extended_fetchers + end + end + + defp add_chain_type_dependent_fetchers(fetchers) do + case Application.get_env(:explorer, :chain_type) do + "stability" -> + [{ValidatorStability, []} | fetchers] + + _ -> + fetchers + end + end + defp configure(process, opts) do if Application.get_env(:indexer, process)[:enabled] do [{process, opts}] diff --git a/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex b/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex index 17f676f136d5..d048a3331172 100644 --- a/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex +++ b/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex @@ -12,7 +12,7 @@ defmodule Indexer.Temporary.UncatalogedTokenTransfers do require Logger - alias Explorer.Chain + alias Explorer.Chain.TokenTransfer alias Indexer.Block.Catchup.Fetcher alias Indexer.Temporary.UncatalogedTokenTransfers @@ -52,7 +52,7 @@ defmodule Indexer.Temporary.UncatalogedTokenTransfers do end def handle_info(:scan, state) do - {:ok, block_numbers} = Chain.uncataloged_token_transfer_block_numbers() + {:ok, block_numbers} = TokenTransfer.uncataloged_token_transfer_block_numbers() case block_numbers do [] -> diff --git a/apps/indexer/lib/indexer/token_balances.ex b/apps/indexer/lib/indexer/token_balances.ex index e53347f90ba9..066cddbac24b 100644 --- a/apps/indexer/lib/indexer/token_balances.ex +++ b/apps/indexer/lib/indexer/token_balances.ex @@ -13,7 +13,7 @@ defmodule Indexer.TokenBalances do alias Indexer.Fetcher.TokenBalance alias Indexer.Tracer - @erc1155_balance_function_abi [ + @nft_balance_function_abi [ %{ "constant" => true, "inputs" => [%{"name" => "_owner", "type" => "address"}, %{"name" => "_id", "type" => "uint256"}], @@ -39,7 +39,7 @@ defmodule Indexer.TokenBalances do * `address_hash` - The address_hash that we want to know the balance. * `block_number` - The block number that the address_hash has the balance. * `token_type` - type of the token that balance belongs to - * `token_id` - token id for ERC-1155 tokens + * `token_id` - token id for ERC-1155/ERC-404 tokens """ def fetch_token_balances_from_blockchain([]), do: {:ok, []} @@ -47,39 +47,39 @@ defmodule Indexer.TokenBalances do def fetch_token_balances_from_blockchain(token_balances) do Logger.debug("fetching token balances", count: Enum.count(token_balances)) - regular_token_balances = + ft_token_balances = token_balances - |> Enum.filter(fn request -> - if Map.has_key?(request, :token_type) do - request.token_type !== "ERC-1155" + |> Enum.filter(fn token_balance -> + if Map.has_key?(token_balance, :token_type) do + token_balance.token_type !== "ERC-1155" && !(token_balance.token_type == "ERC-404" && token_balance.token_id) else true end end) - erc1155_token_balances = + nft_token_balances = token_balances - |> Enum.filter(fn request -> - if Map.has_key?(request, :token_type) do - request.token_type == "ERC-1155" + |> Enum.filter(fn token_balance -> + if Map.has_key?(token_balance, :token_type) do + token_balance.token_type == "ERC-1155" || (token_balance.token_type == "ERC-404" && token_balance.token_id) else false end end) - requested_regular_token_balances = - regular_token_balances + requested_ft_token_balances = + ft_token_balances |> BalanceReader.get_balances_of() - |> Stream.zip(regular_token_balances) + |> Stream.zip(ft_token_balances) |> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end) - requested_erc1155_token_balances = - erc1155_token_balances - |> BalanceReader.get_balances_of_with_abi(@erc1155_balance_function_abi) - |> Stream.zip(erc1155_token_balances) + requested_nft_token_balances = + nft_token_balances + |> BalanceReader.get_balances_of_with_abi(@nft_balance_function_abi) + |> Stream.zip(nft_token_balances) |> Enum.map(fn {result, token_balance} -> set_token_balance_value(result, token_balance) end) - requested_token_balances = requested_regular_token_balances ++ requested_erc1155_token_balances + requested_token_balances = requested_ft_token_balances ++ requested_nft_token_balances fetched_token_balances = Enum.filter(requested_token_balances, &ignore_request_with_errors/1) requested_token_balances diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances.ex b/apps/indexer/lib/indexer/transform/address_coin_balances.ex index 4c7a20ca66ea..9f1935da4638 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances.ex @@ -32,7 +32,11 @@ defmodule Indexer.Transform.AddressCoinBalances do defp reducer({:logs_params, logs_params}, acc) when is_list(logs_params) do # a log MUST have address_hash and block_number logs_params - |> Enum.reject(&(&1.first_topic == TokenTransfer.constant())) + |> Enum.reject( + &(&1.first_topic == TokenTransfer.constant() or + &1.first_topic == TokenTransfer.erc1155_single_transfer_signature() or + &1.first_topic == TokenTransfer.erc1155_batch_transfer_signature()) + ) |> Enum.into(acc, fn %{address_hash: address_hash, block_number: block_number} when is_binary(address_hash) and is_integer(block_number) -> diff --git a/apps/indexer/lib/indexer/transform/address_token_balances.ex b/apps/indexer/lib/indexer/transform/address_token_balances.ex index 291783f6a2ba..af0c37caa7ef 100644 --- a/apps/indexer/lib/indexer/transform/address_token_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_token_balances.ex @@ -22,7 +22,10 @@ defmodule Indexer.Transform.AddressTokenBalances do acc when is_integer(block_number) and is_binary(from_address_hash) and is_binary(to_address_hash) and is_binary(token_contract_address_hash) -> - Enum.reduce(token_ids || [nil], acc, fn id, sub_acc -> + sanitized_token_ids = + if is_nil(token_ids) || (is_list(token_ids) && Enum.empty?(token_ids)), do: [nil], else: token_ids + + Enum.reduce(sanitized_token_ids, acc, fn id, sub_acc -> sub_acc |> add_token_balance_address(from_address_hash, token_contract_address_hash, id, token_type, block_number) |> add_token_balance_address(to_address_hash, token_contract_address_hash, id, token_type, block_number) diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex index bd31681200da..af645ba34457 100644 --- a/apps/indexer/lib/indexer/transform/addresses.ex +++ b/apps/indexer/lib/indexer/transform/addresses.ex @@ -48,6 +48,8 @@ defmodule Indexer.Transform.Addresses do } """ + alias Indexer.Helper + @entity_to_address_map %{ address_coin_balances: [ [ @@ -95,7 +97,9 @@ defmodule Indexer.Transform.Addresses do ], [ %{from: :block_number, to: :fetched_coin_balance_block_number}, - %{from: :to_address_hash, to: :hash}, + %{from: :to_address_hash, to: :hash} + ], + [ %{from: :execution_node_hash, to: :hash}, %{from: :wrapped_to_address_hash, to: :hash} ] @@ -106,6 +110,11 @@ defmodule Indexer.Transform.Addresses do %{from: :address_hash, to: :hash} ] ], + shibarium_bridge_operations: [ + [ + %{from: :user, to: :hash} + ] + ], token_transfers: [ [ %{from: :block_number, to: :fetched_coin_balance_block_number}, @@ -141,6 +150,11 @@ defmodule Indexer.Transform.Addresses do %{from: :block_number, to: :fetched_coin_balance_block_number}, %{from: :address_hash, to: :hash} ] + ], + polygon_zkevm_bridge_operations: [ + [ + %{from: :l2_token_address, to: :hash} + ] ] } @@ -412,6 +426,11 @@ defmodule Indexer.Transform.Addresses do required(:block_number) => non_neg_integer() } ], + optional(:shibarium_bridge_operations) => [ + %{ + required(:user) => String.t() + } + ], optional(:token_transfers) => [ %{ required(:from_address_hash) => String.t(), @@ -443,6 +462,11 @@ defmodule Indexer.Transform.Addresses do required(:address_hash) => String.t(), required(:block_number) => non_neg_integer() } + ], + optional(:polygon_zkevm_bridge_operations) => [ + %{ + optional(:l2_token_address) => String.t() + } ] }) :: [params] def extract_addresses(fetched_data, options \\ []) when is_map(fetched_data) and is_list(options) do @@ -484,7 +508,7 @@ defmodule Indexer.Transform.Addresses do end defp find_tx_action_addresses(block_number, value, accumulator) when is_binary(value) do - if is_address?(value) do + if Helper.address_correct?(value) do [%{:fetched_coin_balance_block_number => block_number, :hash => value} | accumulator] else accumulator @@ -577,8 +601,4 @@ defmodule Indexer.Transform.Addresses do defp max_nil_last(first_integer, second_integer) when is_integer(first_integer) and is_integer(second_integer), do: max(first_integer, second_integer) - - defp is_address?(value) when is_binary(value) do - String.match?(value, ~r/^0x[[:xdigit:]]{40}$/i) - end end diff --git a/apps/indexer/lib/indexer/transform/mint_transfers.ex b/apps/indexer/lib/indexer/transform/mint_transfers.ex index e57a9841a7fb..c53dde7a5387 100644 --- a/apps/indexer/lib/indexer/transform/mint_transfers.ex +++ b/apps/indexer/lib/indexer/transform/mint_transfers.ex @@ -24,8 +24,7 @@ defmodule Indexer.Transform.MintTransfers do ...> index: 1, ...> second_topic: "0x0000000000000000000000009a4a90e2732f3fa4087b0bb4bf85c76d14833df1", ...> third_topic: "0x0000000000000000000000007301cfa0e1756b71869e93d4e4dca5c7d0eb0aa6", - ...> transaction_hash: "0x1d5066d30ff3404a9306733136103ac2b0b989951c38df637f464f3667f8d4ee", - ...> type: "mined" + ...> transaction_hash: "0x1d5066d30ff3404a9306733136103ac2b0b989951c38df637f464f3667f8d4ee" ...> } ...> ]) %{ diff --git a/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex b/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex new file mode 100644 index 000000000000..f82dd4fcdd84 --- /dev/null +++ b/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex @@ -0,0 +1,49 @@ +defmodule Indexer.Transform.Optimism.Withdrawals do + @moduledoc """ + Helper functions for transforming data for Optimism withdrawals. + """ + + require Logger + + alias Indexer.Fetcher.Optimism.Withdrawal, as: OptimismWithdrawal + alias Indexer.Helper + + # 32-byte signature of the event MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash) + @message_passed_event "0x02a52367d10742d8032712c1bb8e0144ff1ec5ffda1ed7d70bb05a2744955054" + + @doc """ + Returns a list of withdrawals given a list of logs. + """ + def parse(logs) do + prev_metadata = Logger.metadata() + Logger.metadata(fetcher: :optimism_withdrawals_realtime) + + items = + with false <- is_nil(Application.get_env(:indexer, Indexer.Fetcher.OptimismWithdrawal)[:start_block_l2]), + message_passer = Application.get_env(:indexer, Indexer.Fetcher.OptimismWithdrawal)[:message_passer], + true <- Helper.address_correct?(message_passer) do + message_passer = String.downcase(message_passer) + + logs + |> Enum.filter(fn log -> + !is_nil(log.first_topic) && String.downcase(log.first_topic) == @message_passed_event && + String.downcase(Helper.address_hash_to_string(log.address_hash)) == message_passer + end) + |> Enum.map(fn log -> + Logger.info("Withdrawal message found, nonce: #{log.second_topic}.") + OptimismWithdrawal.event_to_withdrawal(log.second_topic, log.data, log.transaction_hash, log.block_number) + end) + else + true -> + [] + + false -> + Logger.error("L2ToL1MessagePasser contract address is incorrect. Cannot use #{__MODULE__} for parsing logs.") + [] + end + + Logger.reset_metadata(prev_metadata) + + items + end +end diff --git a/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex b/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex index 93dec160353c..0e3ddeae5520 100644 --- a/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex +++ b/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex @@ -20,7 +20,7 @@ defmodule Indexer.Transform.PolygonEdge.DepositExecutes do with false <- is_nil(Application.get_env(:indexer, DepositExecute)[:start_block_l2]), state_receiver = Application.get_env(:indexer, DepositExecute)[:state_receiver], - true <- Helper.is_address_correct?(state_receiver) do + true <- Helper.address_correct?(state_receiver) do state_receiver = String.downcase(state_receiver) state_sync_result_event_signature = DepositExecute.state_sync_result_event_signature() diff --git a/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex b/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex index 1562f1fea61d..02ffcbaaa71e 100644 --- a/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex +++ b/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex @@ -19,7 +19,7 @@ defmodule Indexer.Transform.PolygonEdge.Withdrawals do items = with false <- is_nil(Application.get_env(:indexer, Withdrawal)[:start_block_l2]), state_sender = Application.get_env(:indexer, Withdrawal)[:state_sender], - true <- Helper.is_address_correct?(state_sender) do + true <- Helper.address_correct?(state_sender) do state_sender = String.downcase(state_sender) l2_state_synced_event_signature = Withdrawal.l2_state_synced_event_signature() diff --git a/apps/indexer/lib/indexer/transform/polygon_zkevm/bridge.ex b/apps/indexer/lib/indexer/transform/polygon_zkevm/bridge.ex new file mode 100644 index 000000000000..4ee7fa4126ef --- /dev/null +++ b/apps/indexer/lib/indexer/transform/polygon_zkevm/bridge.ex @@ -0,0 +1,77 @@ +defmodule Indexer.Transform.PolygonZkevm.Bridge do + @moduledoc """ + Helper functions for transforming data for Polygon zkEVM Bridge operations. + """ + + require Logger + + import Indexer.Fetcher.PolygonZkevm.Bridge, + only: [filter_bridge_events: 2, prepare_operations: 4] + + alias Indexer.Fetcher.PolygonZkevm.{BridgeL1, BridgeL2} + alias Indexer.Helper + + @doc """ + Returns a list of operations given a list of blocks and logs. + """ + @spec parse(list(), list()) :: list() + def parse(blocks, logs) do + prev_metadata = Logger.metadata() + Logger.metadata(fetcher: :polygon_zkevm_bridge_l2_realtime) + + items = + with false <- is_nil(Application.get_env(:indexer, BridgeL2)[:start_block]), + false <- Application.get_env(:explorer, :chain_type) != "polygon_zkevm", + rpc_l1 = Application.get_all_env(:indexer)[BridgeL1][:rpc], + {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(rpc_l1)}, + bridge_contract = Application.get_env(:indexer, BridgeL2)[:bridge_contract], + {:bridge_contract_address_is_valid, true} <- + {:bridge_contract_address_is_valid, Helper.address_correct?(bridge_contract)} do + bridge_contract = String.downcase(bridge_contract) + + block_numbers = Enum.map(blocks, fn block -> block.number end) + start_block = Enum.min(block_numbers) + end_block = Enum.max(block_numbers) + + Helper.log_blocks_chunk_handling(start_block, end_block, start_block, end_block, nil, :L2) + + json_rpc_named_arguments_l1 = Helper.json_rpc_named_arguments(rpc_l1) + + block_to_timestamp = Enum.reduce(blocks, %{}, fn block, acc -> Map.put(acc, block.number, block.timestamp) end) + + items = + logs + |> filter_bridge_events(bridge_contract) + |> prepare_operations(nil, json_rpc_named_arguments_l1, block_to_timestamp) + + Helper.log_blocks_chunk_handling( + start_block, + end_block, + start_block, + end_block, + "#{Enum.count(items)} L2 operation(s)", + :L2 + ) + + items + else + true -> + [] + + {:rpc_l1_undefined, true} -> + Logger.error("L1 RPC URL is not defined. Cannot use #{__MODULE__} for parsing logs.") + [] + + {:bridge_contract_address_is_valid, false} -> + Logger.error( + "PolygonZkEVMBridge contract address is invalid or not defined. Cannot use #{__MODULE__} for parsing logs." + ) + + [] + end + + Logger.reset_metadata(prev_metadata) + + items + end +end diff --git a/apps/indexer/lib/indexer/transform/shibarium/bridge.ex b/apps/indexer/lib/indexer/transform/shibarium/bridge.ex new file mode 100644 index 000000000000..ddf734507ecf --- /dev/null +++ b/apps/indexer/lib/indexer/transform/shibarium/bridge.ex @@ -0,0 +1,99 @@ +defmodule Indexer.Transform.Shibarium.Bridge do + @moduledoc """ + Helper functions for transforming data for Shibarium Bridge operations. + """ + + require Logger + + import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] + + import Indexer.Fetcher.Shibarium.Helper, only: [prepare_insert_items: 2] + + import Indexer.Fetcher.Shibarium.L2, only: [withdraw_method_signature: 0] + + alias Indexer.Fetcher.Shibarium.L2 + alias Indexer.Helper + + @doc """ + Returns a list of operations given a list of blocks and their transactions. + """ + @spec parse(list(), list(), list()) :: list() + def parse(blocks, transactions_with_receipts, logs) do + prev_metadata = Logger.metadata() + Logger.metadata(fetcher: :shibarium_bridge_l2_realtime) + + items = + with false <- is_nil(Application.get_env(:indexer, Indexer.Fetcher.Shibarium.L2)[:start_block]), + false <- Application.get_env(:explorer, :chain_type) != "shibarium", + child_chain = Application.get_env(:indexer, Indexer.Fetcher.Shibarium.L2)[:child_chain], + weth = Application.get_env(:indexer, Indexer.Fetcher.Shibarium.L2)[:weth], + bone_withdraw = Application.get_env(:indexer, Indexer.Fetcher.Shibarium.L2)[:bone_withdraw], + true <- Helper.address_correct?(child_chain), + true <- Helper.address_correct?(weth), + true <- Helper.address_correct?(bone_withdraw) do + child_chain = String.downcase(child_chain) + weth = String.downcase(weth) + bone_withdraw = String.downcase(bone_withdraw) + + block_numbers = Enum.map(blocks, fn block -> block.number end) + start_block = Enum.min(block_numbers) + end_block = Enum.max(block_numbers) + + Helper.log_blocks_chunk_handling(start_block, end_block, start_block, end_block, nil, :L2) + + deposit_transaction_hashes = + transactions_with_receipts + |> Enum.filter(fn tx -> tx.from_address_hash == burn_address_hash_string() end) + |> Enum.map(fn tx -> tx.hash end) + + deposit_events = + logs + |> Enum.filter(&Enum.member?(deposit_transaction_hashes, &1.transaction_hash)) + |> L2.filter_deposit_events(child_chain) + + withdrawal_transaction_hashes = + transactions_with_receipts + |> Enum.filter(fn tx -> + # filter by `withdraw(uint256 amount)` signature + String.downcase(String.slice(tx.input, 0..9)) == withdraw_method_signature() + end) + |> Enum.map(fn tx -> tx.hash end) + + withdrawal_events = + logs + |> Enum.filter(&Enum.member?(withdrawal_transaction_hashes, &1.transaction_hash)) + |> L2.filter_withdrawal_events(bone_withdraw) + + events = deposit_events ++ withdrawal_events + timestamps = Enum.reduce(blocks, %{}, fn block, acc -> Map.put(acc, block.number, block.timestamp) end) + + operations = L2.prepare_operations({events, timestamps}, weth) + items = prepare_insert_items(operations, L2) + + Helper.log_blocks_chunk_handling( + start_block, + end_block, + start_block, + end_block, + "#{Enum.count(operations)} L2 operation(s)", + :L2 + ) + + items + else + true -> + [] + + false -> + Logger.error( + "ChildChain or WETH or BoneWithdraw contract address is incorrect. Cannot use #{__MODULE__} for parsing logs." + ) + + [] + end + + Logger.reset_metadata(prev_metadata) + + items + end +end diff --git a/apps/indexer/lib/indexer/transform/token_instances.ex b/apps/indexer/lib/indexer/transform/token_instances.ex index a9fb4372d2fd..1b9374318b7d 100644 --- a/apps/indexer/lib/indexer/transform/token_instances.ex +++ b/apps/indexer/lib/indexer/transform/token_instances.ex @@ -51,27 +51,24 @@ defmodule Indexer.Transform.TokenInstances do current_key = {token_contract_address_hash, token_id} - Map.put( + Map.update( acc, current_key, - Enum.max_by( - [ - params, - acc[current_key] || params - ], - fn %{ - owner_updated_at_block: owner_updated_at_block, - owner_updated_at_log_index: owner_updated_at_log_index - } -> - {owner_updated_at_block, owner_updated_at_log_index} - end - ) + params, + fn current -> + Enum.max_by([params, current], fn ti -> + { + Map.get(ti, :owner_updated_at_block, 0), + Map.get(ti, :owner_updated_at_log_index, 0) + } + end) + end ) end defp transfer_to_instances( %{ - token_type: _token_type, + token_type: "ERC-404" = token_type, token_ids: [_ | _] = token_ids, token_contract_address_hash: token_contract_address_hash }, @@ -79,6 +76,23 @@ defmodule Indexer.Transform.TokenInstances do ) do Enum.reduce(token_ids, acc, fn id, sub_acc -> Map.put(sub_acc, {token_contract_address_hash, id}, %{ + token_contract_address_hash: token_contract_address_hash, + token_id: id, + token_type: token_type + }) + end) + end + + defp transfer_to_instances( + %{ + token_type: _token_type, + token_ids: [_ | _] = token_ids, + token_contract_address_hash: token_contract_address_hash + }, + acc + ) do + Enum.reduce(token_ids, acc, fn id, sub_acc -> + Map.put_new(sub_acc, {token_contract_address_hash, id}, %{ token_contract_address_hash: token_contract_address_hash, token_id: id, token_type: "ERC-1155" diff --git a/apps/indexer/lib/indexer/transform/token_transfers.ex b/apps/indexer/lib/indexer/transform/token_transfers.ex index fc6fb6a6cfe7..2ffd90abd1f5 100644 --- a/apps/indexer/lib/indexer/transform/token_transfers.ex +++ b/apps/indexer/lib/indexer/transform/token_transfers.ex @@ -1,15 +1,14 @@ defmodule Indexer.Transform.TokenTransfers do @moduledoc """ - Helper functions for transforming data for ERC-20 and ERC-721 token transfers. + Helper functions for transforming data for known token standards (ERC-20, ERC-721, ERC-1155, ERC-404) transfers. """ require Logger import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - import Explorer.Helper, only: [decode_data: 2] - alias Explorer.Repo - alias Explorer.Chain.{Token, TokenTransfer} + alias Explorer.{Helper, Repo} + alias Explorer.Chain.{Hash, Token, TokenTransfer} alias Indexer.Fetcher.TokenTotalSupplyUpdater @doc """ @@ -39,15 +38,26 @@ defmodule Indexer.Transform.TokenTransfers do end) |> Enum.reduce(initial_acc, &do_parse(&1, &2, :erc1155)) + erc404_token_transfers = + logs + |> Enum.filter(fn log -> + log.first_topic == TokenTransfer.erc404_erc20_transfer_event() || + log.first_topic == TokenTransfer.erc404_erc721_transfer_event() + end) + |> Enum.reduce(initial_acc, &do_parse(&1, &2, :erc404)) + rough_tokens = - erc1155_token_transfers.tokens ++ + erc404_token_transfers.tokens ++ + erc1155_token_transfers.tokens ++ erc20_and_erc721_token_transfers.tokens ++ weth_transfers.tokens rough_token_transfers = - erc1155_token_transfers.token_transfers ++ + erc404_token_transfers.token_transfers ++ + erc1155_token_transfers.token_transfers ++ erc20_and_erc721_token_transfers.token_transfers ++ weth_transfers.token_transfers - {tokens, token_transfers} = sanitize_token_types(rough_tokens, rough_token_transfers) + tokens = sanitize_token_types(rough_tokens, rough_token_transfers) + token_transfers = sanitize_weth_transfers(tokens, rough_token_transfers, weth_transfers.token_transfers) token_transfers |> Enum.filter(fn token_transfer -> @@ -70,9 +80,43 @@ defmodule Indexer.Transform.TokenTransfers do token_transfers_from_logs_uniq end + defp sanitize_weth_transfers(total_tokens, total_transfers, weth_transfers) do + existing_token_types_map = + total_tokens + |> Enum.map(&{&1.contract_address_hash, &1.type}) + |> Map.new() + + invalid_weth_transfers = + Enum.reduce(weth_transfers, %{}, fn token_transfer, acc -> + if existing_token_types_map[token_transfer.token_contract_address_hash] == "ERC-721" do + Map.put(acc, token_transfer_to_key(token_transfer), true) + else + acc + end + end) + + total_transfers + |> subtract_token_transfers(invalid_weth_transfers) + |> Enum.reverse() + end + + defp token_transfer_to_key(token_transfer) do + {token_transfer.block_hash, token_transfer.transaction_hash, token_transfer.log_index} + end + + defp subtract_token_transfers(tt_from, tt_to_subtract) do + Enum.reduce(tt_from, [], fn tt, acc -> + case tt_to_subtract[token_transfer_to_key(tt)] do + nil -> [tt | acc] + _ -> acc + end + end) + end + defp sanitize_token_types(tokens, token_transfers) do existing_token_types_map = tokens + |> Enum.uniq() |> Enum.reduce([], fn %{contract_address_hash: address_hash}, acc -> case Repo.get_by(Token, contract_address_hash: address_hash) do %{type: type} -> [{address_hash, type} | acc] @@ -81,34 +125,22 @@ defmodule Indexer.Transform.TokenTransfers do end) |> Map.new() - existing_tokens = - existing_token_types_map - |> Map.keys() - |> Enum.map(&to_string/1) - - new_tokens_token_transfers = Enum.filter(token_transfers, &(&1.token_contract_address_hash not in existing_tokens)) - - new_token_types_map = - new_tokens_token_transfers + token_types_map = + token_transfers |> Enum.group_by(& &1.token_contract_address_hash) |> Enum.map(fn {contract_address_hash, transfers} -> {contract_address_hash, define_token_type(transfers)} end) |> Map.new() - actual_token_types_map = Map.merge(new_token_types_map, existing_token_types_map) - - actual_tokens = - Enum.map(tokens, fn %{contract_address_hash: hash} = token -> - Map.put(token, :type, actual_token_types_map[hash]) + actual_token_types_map = + Map.merge(token_types_map, existing_token_types_map, fn _k, new_type, old_type -> + if token_type_priority(old_type) > token_type_priority(new_type), do: old_type, else: new_type end) - actual_token_transfers = - Enum.map(token_transfers, fn %{token_contract_address_hash: hash} = tt -> - Map.put(tt, :token_type, actual_token_types_map[hash]) - end) - - {actual_tokens, actual_token_transfers} + Enum.map(tokens, fn %{contract_address_hash: hash} = token -> + Map.put(token, :type, actual_token_types_map[hash]) + end) end defp define_token_type(token_transfers) do @@ -119,17 +151,17 @@ defmodule Indexer.Transform.TokenTransfers do defp token_type_priority(nil), do: -1 - @token_types_priority_order ["ERC-20", "ERC-721", "ERC-1155"] + @token_types_priority_order ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] defp token_type_priority(token_type) do Enum.find_index(@token_types_priority_order, &(&1 == token_type)) end defp do_parse(log, %{tokens: tokens, token_transfers: token_transfers} = acc, type \\ :erc20_erc721) do parse_result = - if type != :erc1155 do - parse_params(log) - else - parse_erc1155_params(log) + case type do + :erc1155 -> parse_erc1155_params(log) + :erc404 -> parse_erc404_params(log) + _ -> parse_params(log) end case parse_result do @@ -154,15 +186,18 @@ defmodule Indexer.Transform.TokenTransfers do # ERC-20 token transfer defp parse_params(%{second_topic: second_topic, third_topic: third_topic, fourth_topic: nil} = log) when not is_nil(second_topic) and not is_nil(third_topic) do - [amount] = decode_data(log.data, [{:uint, 256}]) + [amount] = Helper.decode_data(log.data, [{:uint, 256}]) + + from_address_hash = truncate_address_hash(log.second_topic) + to_address_hash = truncate_address_hash(log.third_topic) token_transfer = %{ amount: Decimal.new(amount || 0), block_number: log.block_number, block_hash: log.block_hash, log_index: log.index, - from_address_hash: truncate_address_hash(log.second_topic), - to_address_hash: truncate_address_hash(log.third_topic), + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, token_contract_address_hash: log.address_hash, transaction_hash: log.transaction_hash, token_ids: nil, @@ -180,7 +215,7 @@ defmodule Indexer.Transform.TokenTransfers do # ERC-20 token transfer for WETH defp parse_params(%{second_topic: second_topic, third_topic: nil, fourth_topic: nil} = log) when not is_nil(second_topic) do - [amount] = decode_data(log.data, [{:uint, 256}]) + [amount] = Helper.decode_data(log.data, [{:uint, 256}]) {from_address_hash, to_address_hash} = if log.first_topic == TokenTransfer.weth_deposit_signature() do @@ -213,14 +248,17 @@ defmodule Indexer.Transform.TokenTransfers do # ERC-721 token transfer with topics as addresses defp parse_params(%{second_topic: second_topic, third_topic: third_topic, fourth_topic: fourth_topic} = log) when not is_nil(second_topic) and not is_nil(third_topic) and not is_nil(fourth_topic) do - [token_id] = decode_data(fourth_topic, [{:uint, 256}]) + [token_id] = Helper.decode_data(fourth_topic, [{:uint, 256}]) + + from_address_hash = truncate_address_hash(log.second_topic) + to_address_hash = truncate_address_hash(log.third_topic) token_transfer = %{ block_number: log.block_number, log_index: log.index, block_hash: log.block_hash, - from_address_hash: truncate_address_hash(log.second_topic), - to_address_hash: truncate_address_hash(log.third_topic), + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, token_contract_address_hash: log.address_hash, token_ids: [token_id || 0], transaction_hash: log.transaction_hash, @@ -245,7 +283,7 @@ defmodule Indexer.Transform.TokenTransfers do } = log ) when not is_nil(data) do - [from_address_hash, to_address_hash, token_id] = decode_data(data, [:address, :address, {:uint, 256}]) + [from_address_hash, to_address_hash, token_id] = Helper.decode_data(data, [:address, :address, {:uint, 256}]) token_transfer = %{ block_number: log.block_number, @@ -267,25 +305,34 @@ defmodule Indexer.Transform.TokenTransfers do {token, token_transfer} end - def parse_erc1155_params( - %{ - first_topic: unquote(TokenTransfer.erc1155_batch_transfer_signature()), - third_topic: third_topic, - fourth_topic: fourth_topic, - data: data - } = log - ) do - [token_ids, values] = decode_data(data, [{:array, {:uint, 256}}, {:array, {:uint, 256}}]) - - if token_ids == [] || values == [] do + @spec parse_erc1155_params(map()) :: + nil + | {%{ + contract_address_hash: Hash.Address.t(), + type: String.t() + }, map()} + defp parse_erc1155_params( + %{ + first_topic: unquote(TokenTransfer.erc1155_batch_transfer_signature()), + third_topic: third_topic, + fourth_topic: fourth_topic, + data: data + } = log + ) do + [token_ids, values] = Helper.decode_data(data, [{:array, {:uint, 256}}, {:array, {:uint, 256}}]) + + if is_nil(token_ids) or token_ids == [] or is_nil(values) or values == [] do nil else + from_address_hash = truncate_address_hash(third_topic) + to_address_hash = truncate_address_hash(fourth_topic) + token_transfer = %{ block_number: log.block_number, block_hash: log.block_hash, log_index: log.index, - from_address_hash: truncate_address_hash(third_topic), - to_address_hash: truncate_address_hash(fourth_topic), + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, token_contract_address_hash: log.address_hash, transaction_hash: log.transaction_hash, token_type: "ERC-1155", @@ -302,16 +349,19 @@ defmodule Indexer.Transform.TokenTransfers do end end - def parse_erc1155_params(%{third_topic: third_topic, fourth_topic: fourth_topic, data: data} = log) do - [token_id, value] = decode_data(data, [{:uint, 256}, {:uint, 256}]) + defp parse_erc1155_params(%{third_topic: third_topic, fourth_topic: fourth_topic, data: data} = log) do + [token_id, value] = Helper.decode_data(data, [{:uint, 256}, {:uint, 256}]) + + from_address_hash = truncate_address_hash(third_topic) + to_address_hash = truncate_address_hash(fourth_topic) token_transfer = %{ amount: value, block_number: log.block_number, block_hash: log.block_hash, log_index: log.index, - from_address_hash: truncate_address_hash(third_topic), - to_address_hash: truncate_address_hash(fourth_topic), + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, token_contract_address_hash: log.address_hash, transaction_hash: log.transaction_hash, token_type: "ERC-1155", @@ -326,6 +376,84 @@ defmodule Indexer.Transform.TokenTransfers do {token, token_transfer} end + @spec parse_erc404_params(map()) :: + nil + | {%{ + contract_address_hash: Hash.Address.t(), + type: String.t() + }, map()} + defp parse_erc404_params( + %{ + first_topic: unquote(TokenTransfer.erc404_erc20_transfer_event()), + second_topic: second_topic, + third_topic: third_topic, + fourth_topic: nil, + data: data + } = log + ) do + [value] = Helper.decode_data(data, [{:uint, 256}]) + + if is_nil(value) or value == [] do + nil + else + token_transfer = %{ + block_number: log.block_number, + block_hash: log.block_hash, + log_index: log.index, + from_address_hash: truncate_address_hash(second_topic), + to_address_hash: truncate_address_hash(third_topic), + token_contract_address_hash: log.address_hash, + transaction_hash: log.transaction_hash, + token_type: "ERC-404", + token_ids: [], + amounts: [value] + } + + token = %{ + contract_address_hash: log.address_hash, + type: "ERC-404" + } + + {token, token_transfer} + end + end + + defp parse_erc404_params( + %{ + first_topic: unquote(TokenTransfer.erc404_erc721_transfer_event()), + second_topic: second_topic, + third_topic: third_topic, + fourth_topic: fourth_topic, + data: _data + } = log + ) do + [token_id] = Helper.decode_data(fourth_topic, [{:uint, 256}]) + + if is_nil(token_id) or token_id == [] do + nil + else + token_transfer = %{ + block_number: log.block_number, + block_hash: log.block_hash, + log_index: log.index, + from_address_hash: truncate_address_hash(second_topic), + to_address_hash: truncate_address_hash(third_topic), + token_contract_address_hash: log.address_hash, + transaction_hash: log.transaction_hash, + token_type: "ERC-404", + token_ids: [token_id], + amounts: [] + } + + token = %{ + contract_address_hash: log.address_hash, + type: "ERC-404" + } + + {token, token_transfer} + end + end + defp truncate_address_hash(nil), do: burn_address_hash_string() defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do diff --git a/apps/indexer/lib/indexer/transform/transaction_actions.ex b/apps/indexer/lib/indexer/transform/transaction_actions.ex index 1620fd724dd2..8c5e34262b0b 100644 --- a/apps/indexer/lib/indexer/transform/transaction_actions.ex +++ b/apps/indexer/lib/indexer/transform/transaction_actions.ex @@ -285,8 +285,15 @@ defmodule Indexer.Transform.TransactionActions do [debt_amount, collateral_amount, _liquidator, _receive_a_token] = decode_data(log.data, [{:uint, 256}, {:uint, 256}, :address, :bool]) - debt_address = truncate_address_hash(log.third_topic) - collateral_address = truncate_address_hash(log.second_topic) + debt_address = + log.third_topic + |> Helper.log_topic_to_string() + |> truncate_address_hash() + + collateral_address = + log.second_topic + |> Helper.log_topic_to_string() + |> truncate_address_hash() case get_token_data([debt_address, collateral_address]) do false -> @@ -318,7 +325,10 @@ defmodule Indexer.Transform.TransactionActions do defp aave_handle_event(type, amount, log, address_topic, chain_id) when type in ["borrow", "supply", "withdraw", "repay", "flash_loan"] do - address = truncate_address_hash(address_topic) + address = + address_topic + |> Helper.log_topic_to_string() + |> truncate_address_hash() case get_token_data([address]) do false -> @@ -345,7 +355,10 @@ defmodule Indexer.Transform.TransactionActions do end defp aave_handle_event(type, log, address_topic, chain_id) when type in ["enable_collateral", "disable_collateral"] do - address = truncate_address_hash(address_topic) + address = + address_topic + |> Helper.log_topic_to_string() + |> truncate_address_hash() case get_token_data([address]) do false -> @@ -448,12 +461,23 @@ defmodule Indexer.Transform.TransactionActions do |> Enum.reduce(%{}, fn log, acc -> if sanitize_first_topic(log.first_topic) == @uniswap_v3_transfer_nft_event do # This is Transfer event for NFT - from = truncate_address_hash(log.second_topic) + from = + log.second_topic + |> Helper.log_topic_to_string() + |> truncate_address_hash() # credo:disable-for-next-line if from == burn_address_hash_string() do - to = truncate_address_hash(log.third_topic) - [token_id] = decode_data(log.fourth_topic, [{:uint, 256}]) + to = + log.third_topic + |> Helper.log_topic_to_string() + |> truncate_address_hash() + + [token_id] = + log.fourth_topic + |> Helper.log_topic_to_string() + |> decode_data([{:uint, 256}]) + mint_nft_ids = Map.put_new(acc, to, %{ids: [], log_index: log.index}) Map.put(mint_nft_ids, to, %{ @@ -653,10 +677,10 @@ defmodule Indexer.Transform.TransactionActions do end) |> Enum.map(fn {pool_address, pool} -> token0 = - if Helper.is_address_correct?(pool.token0), do: String.downcase(pool.token0), else: burn_address_hash_string() + if Helper.address_correct?(pool.token0), do: String.downcase(pool.token0), else: burn_address_hash_string() token1 = - if Helper.is_address_correct?(pool.token1), do: String.downcase(pool.token1), else: burn_address_hash_string() + if Helper.address_correct?(pool.token1), do: String.downcase(pool.token1), else: burn_address_hash_string() fee = if pool.fee == "", do: 0, else: pool.fee @@ -970,7 +994,7 @@ defmodule Indexer.Transform.TransactionActions do end defp sanitize_first_topic(first_topic) do - if is_nil(first_topic), do: "", else: String.downcase(first_topic) + if is_nil(first_topic), do: "", else: String.downcase(Helper.log_topic_to_string(first_topic)) end defp truncate_address_hash(nil), do: burn_address_hash_string() diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index f388f635319c..6c37e44898b1 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,17 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "5.3.1" + version: "6.3.0", + xref: [ + exclude: [ + Explorer.Chain.Optimism.Deposit, + Explorer.Chain.Optimism.FrameSequence, + Explorer.Chain.Optimism.OutputRoot, + Explorer.Chain.Optimism.TxnBatch, + Explorer.Chain.Optimism.Withdrawal, + Explorer.Chain.Optimism.WithdrawalEvent + ] + ] ] end @@ -55,7 +65,9 @@ defmodule Indexer.MixProject do # Tracing {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog - {:spandex_datadog, "~> 1.0"} + {:spandex_datadog, "~> 1.0"}, + {:logger_json, "~> 5.1"}, + {:varint, "~> 1.4"} ] end diff --git a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs index 774ebfe3a5ab..f55ec9134f95 100644 --- a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs +++ b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs @@ -11,9 +11,9 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do alias Indexer.BoundInterval alias Indexer.Block.Catchup alias Indexer.Block.Catchup.MissingRangesCollector + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.{ - CoinBalance, ContractCode, InternalTransaction, ReplacedTransaction, @@ -228,7 +228,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do previous_batch_block_number = first_catchup_block_number - default_blocks_batch_size Application.put_env(:indexer, :block_ranges, "#{previous_batch_block_number}..#{first_catchup_block_number}") - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -431,7 +431,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do MissingRangesCollector.start_link([]) start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -523,7 +523,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do Application.put_env(:indexer, :block_ranges, "0..0") MissingRangesCollector.start_link([]) start_supervised({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) diff --git a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs index 93f687907ca0..dc3d820e4b79 100644 --- a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs @@ -13,7 +13,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do alias Indexer.Block alias Indexer.Block.Catchup.Fetcher alias Indexer.Block.Catchup.MissingRangesCollector - alias Indexer.Fetcher.{BlockReward, CoinBalance, InternalTransaction, Token, TokenBalance, UncleBlock} + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Indexer.Fetcher.{BlockReward, InternalTransaction, Token, TokenBalance, UncleBlock} @moduletag capture_log: true @@ -46,7 +47,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do end test "fetches uncles asynchronously", %{json_rpc_named_arguments: json_rpc_named_arguments} do - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -148,7 +149,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do Application.put_env(:indexer, Indexer.Block.Catchup.Fetcher, batch_size: 1, concurrency: 10) Application.put_env(:indexer, :block_ranges, "0..1") start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -308,7 +309,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do Application.put_env(:indexer, Indexer.Block.Catchup.Fetcher, batch_size: 1, concurrency: 10) Application.put_env(:indexer, :block_ranges, "0..1") start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -465,7 +466,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do Application.put_env(:indexer, Indexer.Block.Catchup.Fetcher, batch_size: 1, concurrency: 10) Application.put_env(:indexer, :block_ranges, "0..1") start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) diff --git a/apps/indexer/test/indexer/block/fetcher/receipts_test.exs b/apps/indexer/test/indexer/block/fetcher/receipts_test.exs index c70d10761a01..a7c45106551e 100644 --- a/apps/indexer/test/indexer/block/fetcher/receipts_test.exs +++ b/apps/indexer/test/indexer/block/fetcher/receipts_test.exs @@ -51,8 +51,7 @@ defmodule Indexer.Block.Fetcher.ReceiptsTest do "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], "transactionHash" => "0x43bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => "0x0", - "transactionLogIndex" => "0x0", - "type" => "mined" + "transactionLogIndex" => "0x0" } ], "logsBloom" => @@ -82,8 +81,7 @@ defmodule Indexer.Block.Fetcher.ReceiptsTest do "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => "0x0", - "transactionLogIndex" => "0x0", - "type" => "mined" + "transactionLogIndex" => "0x0" } ], "logsBloom" => diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 24be0dfa8803..ca9ae2a099de 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -10,9 +10,9 @@ defmodule Indexer.Block.FetcherTest do alias Explorer.Chain.{Address, Log, Transaction, Wei} alias Indexer.Block.Fetcher alias Indexer.BufferedTask + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.{ - CoinBalance, ContractCode, InternalTransaction, ReplacedTransaction, @@ -49,7 +49,7 @@ defmodule Indexer.Block.FetcherTest do describe "import_range/2" do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -375,8 +375,7 @@ defmodule Indexer.Block.FetcherTest do "topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"], "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => "0x0", - "transactionLogIndex" => "0x0", - "type" => "mined" + "transactionLogIndex" => "0x0" } ], "logsBloom" => @@ -582,7 +581,7 @@ defmodule Indexer.Block.FetcherTest do }} = Fetcher.fetch_and_import_range(block_fetcher, block_number..block_number) wait_for_tasks(InternalTransaction) - wait_for_tasks(CoinBalance) + wait_for_tasks(CoinBalanceCatchup) assert Repo.aggregate(Block, :count, :hash) == 1 assert Repo.aggregate(Address, :count, :hash) == 5 @@ -676,7 +675,7 @@ defmodule Indexer.Block.FetcherTest do }} = Fetcher.fetch_and_import_range(block_fetcher, block_number..block_number) wait_for_tasks(InternalTransaction) - wait_for_tasks(CoinBalance) + wait_for_tasks(CoinBalanceCatchup) assert Repo.aggregate(Chain.Block, :count, :hash) == 1 assert Repo.aggregate(Address, :count, :hash) == 2 diff --git a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs index 9d0459e915bc..d7e4f2354409 100644 --- a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -8,6 +8,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do alias Explorer.Chain.{Address, Transaction, Wei} alias Indexer.Block.Catchup.Sequence alias Indexer.Block.Realtime + alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime alias Indexer.Fetcher.{ContractCode, InternalTransaction, ReplacedTransaction, Token, TokenBalance, UncleBlock} @moduletag capture_log: true @@ -36,6 +37,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do } TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceRealtime.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) %{block_fetcher: block_fetcher, json_rpc_named_arguments: core_json_rpc_named_arguments} end @@ -205,7 +207,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do } ]} end) - |> expect(:json_rpc, 4, fn + |> expect(:json_rpc, 1, fn [ %{id: 0, jsonrpc: "2.0", method: "trace_block", params: ["0x3C365F"]}, %{id: 1, jsonrpc: "2.0", method: "trace_block", params: ["0x3C3660"]} @@ -470,43 +472,6 @@ defmodule Indexer.Block.Realtime.FetcherTest do ] } ]} - - [ - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x40b18103537c0f15d5e137dd8ddd019b84949d16", "0x3C365F"] - }, - %{ - id: 1, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", "0x3C365F"] - }, - %{ - id: 2, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", "0x3C3660"] - }, - %{ - id: 3, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x698bf6943bab687b2756394624aa183f434f65da", "0x3C365F"] - } - ] - ], - _ -> - {:ok, - [ - %{id: 0, jsonrpc: "2.0", result: "0x148adc763b603291685"}, - %{id: 1, jsonrpc: "2.0", result: "0x53474fa377a46000"}, - %{id: 2, jsonrpc: "2.0", result: "0x53507afe51f28000"}, - %{id: 3, jsonrpc: "2.0", result: "0x3e1a95d7517dc197108"} - ]} end) end @@ -514,10 +479,10 @@ defmodule Indexer.Block.Realtime.FetcherTest do %{ inserted: %{ addresses: [ - %Address{hash: first_address_hash, fetched_coin_balance_block_number: 3_946_079}, - %Address{hash: second_address_hash, fetched_coin_balance_block_number: 3_946_079}, - %Address{hash: third_address_hash, fetched_coin_balance_block_number: 3_946_080}, - %Address{hash: fourth_address_hash, fetched_coin_balance_block_number: 3_946_079} + %Address{hash: first_address_hash}, + %Address{hash: second_address_hash}, + %Address{hash: third_address_hash}, + %Address{hash: fourth_address_hash} ], address_coin_balances: [ %{ @@ -710,298 +675,6 @@ defmodule Indexer.Block.Realtime.FetcherTest do } ]} end) - |> expect(:json_rpc, 3, fn - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["0x3C365F", true] - } - ], - _ -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", - "difficulty" => "0xfffffffffffffffffffffffffffffffe", - "extraData" => "0xd583010b088650617269747986312e32372e32826c69", - "gasLimit" => "0x7a1200", - "gasUsed" => "0x2886e", - "hash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", - "number" => "0x3c365f", - "parentHash" => "0x57f6d66e07488defccd5216c4d2968dd6afd3bd32415e284de3b02af6535e8dc", - "receiptsRoot" => "0x111be72e682cea9c93e02f1ef503fb64aa821b2ef510fd9177c49b37d0af98b5", - "sealFields" => [ - "0x841246c63f", - "0xb841ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700", - "size" => "0x33e", - "stateRoot" => "0x7f73f5fb9f891213b671356126c31e9795d038844392c7aa8800ed4f52307209", - "step" => "306628159", - "timestamp" => "0x5b61df3b", - "totalDifficulty" => "0x3c365effffffffffffffffffffffffed7f0362", - "transactions" => [ - %{ - "blockHash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", - "blockNumber" => "0x3c365f", - "chainId" => "0x63", - "condition" => nil, - "creates" => nil, - "from" => "0x40b18103537c0f15d5e137dd8ddd019b84949d16", - "gas" => "0x3d9c5", - "gasPrice" => "0x3b9aca00", - "hash" => "0xd3937e70fab3fb2bfe8feefac36815408bf07de3b9e09fe81114b9a6b17f55c8", - "input" => - "0x8841ac11000000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005", - "nonce" => "0x65b", - "publicKey" => - "0x89c2123ed4b5d141cf1f4b6f5f3d754418f03aea2e870a1c50888d94bf5531f74237e2fea72d0bc198ef213272b62c6869615720757255e6cba087f9db6e759f", - "r" => "0x55a1a93541d7f782f97f6699437bb60fa4606d63760b30c1ee317e648f93995", - "raw" => - "0xf8f582065b843b9aca008303d9c594698bf6943bab687b2756394624aa183f434f65da8901158e4f216242a000b8848841ac11000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000581eaa0055a1a93541d7f782f97f6699437bb60fa4606d63760b30c1ee317e648f93995a06affd4da5eca84fbca2b016c980f861e0af1f8d6535e2fe29d8f96dc0ce358f7", - "s" => "0x6affd4da5eca84fbca2b016c980f861e0af1f8d6535e2fe29d8f96dc0ce358f7", - "standardV" => "0x1", - "to" => "0x698bf6943bab687b2756394624aa183f434f65da", - "transactionIndex" => "0x0", - "v" => "0xea", - "value" => "0x1158e4f216242a000" - } - ], - "transactionsRoot" => "0xd7c39a93eafe0bdcbd1324c13dcd674bed8c9fa8adbf8f95bf6a59788985da6f", - "uncles" => ["0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cd"] - } - } - ]} - - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["0x3C3660", true] - } - ], - _ -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", - "difficulty" => "0xfffffffffffffffffffffffffffffffe", - "extraData" => "0xd583010a068650617269747986312e32362e32826c69", - "gasLimit" => "0x7a1200", - "gasUsed" => "0x0", - "hash" => "0xfb483e511d316fa4072694da3f7abc94b06286406af45061e5e681395bdc6815", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", - "number" => "0x3c3660", - "parentHash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x841246c640", - "0xb84114db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "14db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800", - "size" => "0x243", - "stateRoot" => "0x3174c461989e9f99e08fa9b4ffb8bce8d9a281c8fc9f80694bb9d3acd4f15559", - "step" => "306628160", - "timestamp" => "0x5b61df40", - "totalDifficulty" => "0x3c365fffffffffffffffffffffffffed7f0360", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } - ]} - - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "trace_replayBlockTransactions", - params: [ - "0x3C3660", - ["trace"] - ] - }, - %{ - id: 1, - jsonrpc: "2.0", - method: "trace_replayBlockTransactions", - params: [ - "0x3C365F", - ["trace"] - ] - } - ], - _ -> - {:ok, - [ - %{id: 0, jsonrpc: "2.0", result: []}, - %{ - id: 1, - jsonrpc: "2.0", - result: [ - %{ - "output" => "0x", - "stateDiff" => nil, - "trace" => [ - %{ - "action" => %{ - "callType" => "call", - "from" => "0x40b18103537c0f15d5e137dd8ddd019b84949d16", - "gas" => "0x383ad", - "input" => - "0x8841ac11000000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005", - "to" => "0x698bf6943bab687b2756394624aa183f434f65da", - "value" => "0x1158e4f216242a000" - }, - "result" => %{"gasUsed" => "0x23256", "output" => "0x"}, - "subtraces" => 5, - "traceAddress" => [], - "type" => "call" - }, - %{ - "action" => %{ - "callType" => "call", - "from" => "0x698bf6943bab687b2756394624aa183f434f65da", - "gas" => "0x36771", - "input" => "0x6352211e000000000000000000000000000000000000000000000000000000000000006c", - "to" => "0x11c4469d974f8af5ba9ec99f3c42c07c848c861c", - "value" => "0x0" - }, - "result" => %{ - "gasUsed" => "0x495", - "output" => "0x00000000000000000000000040b18103537c0f15d5e137dd8ddd019b84949d16" - }, - "subtraces" => 0, - "traceAddress" => [0], - "type" => "call" - }, - %{ - "action" => %{ - "callType" => "call", - "from" => "0x698bf6943bab687b2756394624aa183f434f65da", - "gas" => "0x35acb", - "input" => "0x33f30a43000000000000000000000000000000000000000000000000000000000000006c", - "to" => "0x11c4469d974f8af5ba9ec99f3c42c07c848c861c", - "value" => "0x0" - }, - "result" => %{ - "gasUsed" => "0x52d2", - "output" => - "0x00000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000058000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000004f000000000000000000000000000000000000000000000000000000000000004d000000000000000000000000000000000000000000000000000000000000004b000000000000000000000000000000000000000000000000000000000000004f00000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000078000000000000000000000000000000000000000000000000000000005b61df09000000000000000000000000000000000000000000000000000000005b61df5e000000000000000000000000000000000000000000000000000000005b61df8b000000000000000000000000000000000000000000000000000000005b61df2c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000000000000000000000000000000000000000000fd000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000004e0000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000189000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054c65696c61000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002566303430313037303331343330303332333036303933333235303131323036303730373131000000000000000000000000000000000000000000000000000000" - }, - "subtraces" => 0, - "traceAddress" => [1], - "type" => "call" - }, - %{ - "action" => %{ - "callType" => "call", - "from" => "0x698bf6943bab687b2756394624aa183f434f65da", - "gas" => "0x2fc79", - "input" => "0x1b8ef0bb000000000000000000000000000000000000000000000000000000000000006c", - "to" => "0x11c4469d974f8af5ba9ec99f3c42c07c848c861c", - "value" => "0x0" - }, - "result" => %{ - "gasUsed" => "0x10f2", - "output" => "0x0000000000000000000000000000000000000000000000000000000000000013" - }, - "subtraces" => 0, - "traceAddress" => [2], - "type" => "call" - }, - %{ - "action" => %{ - "callType" => "call", - "from" => "0x698bf6943bab687b2756394624aa183f434f65da", - "gas" => "0x2e21f", - "input" => - "0xcf5f87d0000000000000000000000000000000000000000000000000000000000000006c0000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000a", - "to" => "0x11c4469d974f8af5ba9ec99f3c42c07c848c861c", - "value" => "0x0" - }, - "result" => %{"gasUsed" => "0x1ca1", "output" => "0x"}, - "subtraces" => 0, - "traceAddress" => [3], - "type" => "call" - }, - %{ - "action" => %{ - "callType" => "call", - "from" => "0x698bf6943bab687b2756394624aa183f434f65da", - "gas" => "0x8fc", - "input" => "0x", - "to" => "0x40b18103537c0f15d5e137dd8ddd019b84949d16", - "value" => "0x9184e72a000" - }, - "result" => %{"gasUsed" => "0x0", "output" => "0x"}, - "subtraces" => 0, - "traceAddress" => [4], - "type" => "call" - } - ], - "transactionHash" => "0xd3937e70fab3fb2bfe8feefac36815408bf07de3b9e09fe81114b9a6b17f55c8", - "vmTrace" => nil - } - ] - } - ]} - - [ - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x40b18103537c0f15d5e137dd8ddd019b84949d16", "0x3C365F"] - }, - %{ - id: 1, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", "0x3C365F"] - }, - %{ - id: 2, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", "0x3C3660"] - }, - %{ - id: 3, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x698bf6943bab687b2756394624aa183f434f65da", "0x3C365F"] - } - ] - ], - _ -> - {:ok, - [ - %{id: 0, jsonrpc: "2.0", result: "0x148adc763b603291685"}, - %{id: 1, jsonrpc: "2.0", result: "0x53474fa377a46000"}, - %{id: 2, jsonrpc: "2.0", result: "0x53507afe51f28000"}, - %{id: 3, jsonrpc: "2.0", result: "0x3e1a95d7517dc197108"} - ]} - end) end first_expected_reward = %Wei{value: Decimal.new(165_998_000_000_000)} @@ -1011,10 +684,10 @@ defmodule Indexer.Block.Realtime.FetcherTest do %{ inserted: %{ addresses: [ - %Address{hash: first_address_hash, fetched_coin_balance_block_number: 3_946_079}, - %Address{hash: second_address_hash, fetched_coin_balance_block_number: 3_946_079}, - %Address{hash: third_address_hash, fetched_coin_balance_block_number: 3_946_080}, - %Address{hash: fourth_address_hash, fetched_coin_balance_block_number: 3_946_079} + %Address{hash: first_address_hash}, + %Address{hash: second_address_hash}, + %Address{hash: third_address_hash}, + %Address{hash: fourth_address_hash} ], address_coin_balances: [ %{ @@ -1187,27 +860,23 @@ defmodule Indexer.Block.Realtime.FetcherTest do ]} [ - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", "0x3C365F"] - } - ] + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_getBalance", + params: ["0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", "0x3C365F"] + } ], _ -> {:ok, [%{id: 0, jsonrpc: "2.0", result: "0x53474fa377a46000"}]} [ - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", "0x3C3660"] - } - ] + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_getBalance", + params: ["0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", "0x3C3660"] + } ], _ -> {:ok, [%{id: 0, jsonrpc: "2.0", result: "0x53507afe51f28000"}]} @@ -1232,14 +901,12 @@ defmodule Indexer.Block.Realtime.FetcherTest do ]} [ - [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBalance", - params: ["0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", "0x3C365F"] - } - ] + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_getBalance", + params: ["0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", "0x3C365F"] + } ], _ -> {:ok, [%{id: 0, jsonrpc: "2.0", result: "0x53474fa377a46000"}]} diff --git a/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs b/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs new file mode 100644 index 000000000000..3d41909b78a6 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs @@ -0,0 +1,170 @@ +defmodule Indexer.Fetcher.Beacon.BlobTest do + use Explorer.DataCase, async: false + + import Mox + + alias Explorer.Chain.Transaction + alias Explorer.Chain.Beacon.{Blob, Reader} + alias Indexer.Fetcher.Beacon.Blob.Supervisor, as: BlobSupervisor + + setup :verify_on_exit! + setup :set_mox_global + + if Application.compile_env(:explorer, :chain_type) == "ethereum" do + describe "init/1" do + setup do + initial_env = Application.get_env(:indexer, BlobSupervisor) + Application.put_env(:indexer, BlobSupervisor, initial_env |> Keyword.put(:disabled?, false)) + + on_exit(fn -> + Application.put_env(:indexer, BlobSupervisor, initial_env) + end) + end + + test "fetches all missed blob transactions" do + {:ok, now, _} = DateTime.from_iso8601("2024-01-24 00:00:00Z") + block_a = insert(:block, timestamp: now) + block_b = insert(:block, timestamp: now |> Timex.shift(seconds: -120)) + block_c = insert(:block, timestamp: now |> Timex.shift(seconds: -240)) + + blob_a = build(:blob) + blob_b = build(:blob) + blob_c = build(:blob) + blob_d = insert(:blob) + + %Transaction{hash: transaction_a_hash} = insert(:transaction, type: 3) |> with_block(block_a) + %Transaction{hash: transaction_b_hash} = insert(:transaction, type: 3) |> with_block(block_b) + %Transaction{hash: transaction_c_hash} = insert(:transaction, type: 3) |> with_block(block_c) + + insert(:blob_transaction, hash: transaction_a_hash, blob_versioned_hashes: [blob_a.hash, blob_b.hash]) + insert(:blob_transaction, hash: transaction_b_hash, blob_versioned_hashes: [blob_c.hash]) + insert(:blob_transaction, hash: transaction_c_hash, blob_versioned_hashes: [blob_d.hash]) + + assert {:error, :not_found} = Reader.blob(blob_a.hash, true) + assert {:error, :not_found} = Reader.blob(blob_b.hash, true) + assert {:error, :not_found} = Reader.blob(blob_c.hash, true) + assert {:ok, _} = Reader.blob(blob_d.hash, true) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + result_ab = """ + { + "data": [ + { + "index": "0", + "blob": "#{to_string(blob_a.blob_data)}", + "kzg_commitment": "#{to_string(blob_a.kzg_commitment)}", + "kzg_proof": "#{to_string(blob_a.kzg_proof)}" + }, + { + "index": "1", + "blob": "#{to_string(blob_b.blob_data)}", + "kzg_commitment": "#{to_string(blob_b.kzg_commitment)}", + "kzg_proof": "#{to_string(blob_b.kzg_proof)}" + } + ] + } + """ + + result_c = """ + { + "data": [ + { + "index": "0", + "blob": "#{to_string(blob_c.blob_data)}", + "kzg_commitment": "#{to_string(blob_c.kzg_commitment)}", + "kzg_proof": "#{to_string(blob_c.kzg_proof)}" + } + ] + } + """ + + Explorer.Mox.HTTPoison + |> expect(:get, 2, fn url -> + case url do + "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269188" -> + {:ok, %HTTPoison.Response{status_code: 200, body: result_c}} + + "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269198" -> + {:ok, %HTTPoison.Response{status_code: 200, body: result_ab}} + end + end) + + BlobSupervisor.Case.start_supervised!() + + wait_for_results(fn -> + Repo.one!(from(blob in Blob, where: blob.hash == ^blob_a.hash)) + end) + + assert {:ok, _} = Reader.blob(blob_a.hash, true) + assert {:ok, _} = Reader.blob(blob_b.hash, true) + assert {:ok, _} = Reader.blob(blob_c.hash, true) + assert {:ok, _} = Reader.blob(blob_d.hash, true) + + Application.put_env(:explorer, :http_adapter, HTTPoison) + end + end + + describe "async_fetch/1" do + setup do + initial_env = Application.get_env(:indexer, BlobSupervisor) + Application.put_env(:indexer, BlobSupervisor, initial_env |> Keyword.put(:disabled?, false)) + + on_exit(fn -> + Application.put_env(:indexer, BlobSupervisor, initial_env) + end) + end + + test "fetches blobs for block timestamp" do + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + {:ok, now, _} = DateTime.from_iso8601("2024-01-24 00:00:00Z") + block_a = insert(:block, timestamp: now) + + %Blob{ + hash: blob_hash_a, + blob_data: blob_data_a, + kzg_commitment: kzg_commitment_a, + kzg_proof: kzg_proof_a + } = build(:blob) + + result_a = """ + { + "data": [ + { + "index": "0", + "blob": "#{to_string(blob_data_a)}", + "kzg_commitment": "#{to_string(kzg_commitment_a)}", + "kzg_proof": "#{to_string(kzg_proof_a)}" + } + ] + } + """ + + Explorer.Mox.HTTPoison + |> expect(:get, fn "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269198" -> + {:ok, %HTTPoison.Response{status_code: 200, body: result_a}} + end) + + BlobSupervisor.Case.start_supervised!() + + assert :ok = Indexer.Fetcher.Beacon.Blob.async_fetch([block_a.timestamp]) + + wait_for_results(fn -> + Repo.one!(from(blob in Blob, where: blob.hash == ^blob_hash_a)) + end) + + assert {:ok, blob} = Reader.blob(blob_hash_a, true) + + assert %{ + hash: ^blob_hash_a, + blob_data: ^blob_data_a, + kzg_commitment: ^kzg_commitment_a, + kzg_proof: ^kzg_proof_a + } = blob + + Application.put_env(:explorer, :http_adapter, HTTPoison) + end + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/block_reward_test.exs b/apps/indexer/test/indexer/fetcher/block_reward_test.exs index 2cf2c01819d3..82bb612feed2 100644 --- a/apps/indexer/test/indexer/fetcher/block_reward_test.exs +++ b/apps/indexer/test/indexer/fetcher/block_reward_test.exs @@ -132,7 +132,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end) - Process.register(pid, Indexer.Fetcher.CoinBalance) + Process.register(pid, Indexer.Fetcher.CoinBalance.Catchup) assert :ok = BlockReward.async_fetch([block_number]) @@ -205,7 +205,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end) - Process.register(pid, Indexer.Fetcher.CoinBalance) + Process.register(pid, Indexer.Fetcher.CoinBalance.Catchup) assert :ok = BlockReward.async_fetch([block_number]) @@ -340,7 +340,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end) - Process.register(pid, Indexer.Fetcher.CoinBalance) + Process.register(pid, Indexer.Fetcher.CoinBalance.Catchup) assert :ok = BlockReward.run([block_number], json_rpc_named_arguments) @@ -430,7 +430,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end) - Process.register(pid, Indexer.Fetcher.CoinBalance) + Process.register(pid, Indexer.Fetcher.CoinBalance.Catchup) assert :ok = BlockReward.run([block_number], json_rpc_named_arguments) @@ -514,7 +514,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end) - Process.register(pid, Indexer.Fetcher.CoinBalance) + Process.register(pid, Indexer.Fetcher.CoinBalance.Catchup) assert :ok = BlockReward.run([block_number], json_rpc_named_arguments) @@ -651,7 +651,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end) - Process.register(pid, Indexer.Fetcher.CoinBalance) + Process.register(pid, Indexer.Fetcher.CoinBalance.Catchup) assert {:retry, [^error_block_number]} = BlockReward.run([block_number, error_block_number], json_rpc_named_arguments) diff --git a/apps/indexer/test/indexer/fetcher/coin_balance_test.exs b/apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs similarity index 95% rename from apps/indexer/test/indexer/fetcher/coin_balance_test.exs rename to apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs index 708a1e058663..358598a17da4 100644 --- a/apps/indexer/test/indexer/fetcher/coin_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs @@ -1,4 +1,4 @@ -defmodule Indexer.Fetcher.CoinBalanceTest do +defmodule Indexer.Fetcher.CoinBalance.CatchupTest do # MUST be `async: false` so that {:shared, pid} is set for connection to allow CoinBalanceFetcher's self-send to have # connection allowed immediately. use EthereumJSONRPC.Case, async: false @@ -8,7 +8,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do import Mox alias Explorer.Chain.{Address, Hash, Wei} - alias Indexer.Fetcher.CoinBalance + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup @moduletag :capture_log @@ -83,7 +83,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do assert miner.fetched_coin_balance == nil assert miner.fetched_coin_balance_block_number == nil - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) fetched_address = wait(fn -> @@ -151,7 +151,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do block = insert(:block, miner: miner, number: block_number) insert(:unfetched_balance, address_hash: miner.hash, block_number: block_number) - CoinBalance.Supervisor.Case.start_supervised!( + CoinBalanceCatchup.Supervisor.Case.start_supervised!( json_rpc_named_arguments: json_rpc_named_arguments, max_batch_size: 2 ) @@ -225,9 +225,9 @@ defmodule Indexer.Fetcher.CoinBalanceTest do end) end - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - assert :ok = CoinBalance.async_fetch_balances([%{address_hash: hash, block_number: block_number}]) + assert :ok = CoinBalanceCatchup.async_fetch_balances([%{address_hash: hash, block_number: block_number}]) address = wait(fn -> @@ -318,7 +318,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do {:ok, [res2]} end) - case CoinBalance.run(entries, json_rpc_named_arguments) do + case CoinBalanceCatchup.run(entries, json_rpc_named_arguments) do :ok -> balances = Repo.all(from(balance in Address.CoinBalance, where: balance.address_hash == ^hash_data)) @@ -373,7 +373,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do {:ok, [%{id: id, error: %{code: 1, message: "Bad"}}]} end) - assert {:retry, ^entries} = CoinBalance.run(entries, json_rpc_named_arguments) + assert {:retry, ^entries} = CoinBalanceCatchup.run(entries, json_rpc_named_arguments) end test "retries none if all imported and no fetch errors", %{json_rpc_named_arguments: json_rpc_named_arguments} do @@ -401,7 +401,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do {:ok, [res]} end) - assert :ok = CoinBalance.run(entries, json_rpc_named_arguments) + assert :ok = CoinBalanceCatchup.run(entries, json_rpc_named_arguments) end test "retries fetch errors if all imported", %{json_rpc_named_arguments: json_rpc_named_arguments} do @@ -457,7 +457,7 @@ defmodule Indexer.Fetcher.CoinBalanceTest do end) assert {:retry, [{^address_hash_bytes, ^bad_block_number}]} = - CoinBalance.run( + CoinBalanceCatchup.run( [{address_hash_bytes, good_block_number}, {address_hash_bytes, bad_block_number}], json_rpc_named_arguments ) diff --git a/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs b/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs index d345ecee0827..17697b2f7b20 100644 --- a/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs +++ b/apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs @@ -43,18 +43,18 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far # back we'd need to go to get 24 hours in the past) - Enum.each(0..100, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, hours: -(101 - i) * 50)) + Enum.each(0..101, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(102 - i) * 50)) end) - insert(:block, number: 101, timestamp: now) + insert(:block, number: 102, timestamp: now) AverageBlockTime.refresh() - stale_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 100) - current_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 101) + stale_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 101) + current_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 102) - pending_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 101) - insert(:unfetched_balance, address_hash: pending_address.hash, block_number: 102) + pending_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 102) + insert(:unfetched_balance, address_hash: pending_address.hash, block_number: 103) %{stale_address: stale_address, current_address: current_address, pending_address: pending_address} end @@ -68,7 +68,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do test "if the address has not been fetched within the last 24 hours of blocks it is considered stale", %{ stale_address: address } do - assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 101} + assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 102} end test "if the address has been fetched within the last 24 hours of blocks it is considered current", %{ @@ -80,7 +80,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do test "if there is an unfetched balance within the window for an address, it is considered pending", %{ pending_address: pending_address } do - assert CoinBalanceOnDemand.trigger_fetch(pending_address) == {:pending, 102} + assert CoinBalanceOnDemand.trigger_fetch(pending_address) == {:pending, 103} end end @@ -139,18 +139,18 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far # back we'd need to go to get 24 hours in the past) - Enum.each(0..100, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, hours: -(101 - i) * 50)) + Enum.each(0..101, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(102 - i) * 50)) end) - insert(:block, number: 101, timestamp: now) + insert(:block, number: 102, timestamp: now) AverageBlockTime.refresh() :ok end test "a stale address broadcasts the new address" do - address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 100) + address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 101) address_hash = address.hash string_address_hash = to_string(address.hash) @@ -158,14 +158,14 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do %{ id: id, method: "eth_getBalance", - params: [^string_address_hash, "0x65"] + params: [^string_address_hash, "0x66"] } ], _options -> {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} end) - res = eth_block_number_fake_response("0x65") + res = eth_block_number_fake_response("0x66") EthereumJSONRPC.Mox |> expect(:json_rpc, fn [ @@ -173,26 +173,26 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", - params: ["0x65", true] + params: ["0x66", true] } ], _ -> {:ok, [res]} end) - assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 101} + assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 102} {:ok, expected_wei} = Wei.cast(2) assert_receive( {:chain_event, :addresses, :on_demand, - [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 101}]} + [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 102}]} ) end test "a pending address broadcasts the new address and the new coin balance" do - address = insert(:address, fetched_coin_balance: 0, fetched_coin_balance_block_number: 101) - insert(:unfetched_balance, address_hash: address.hash, block_number: 102) + address = insert(:address, fetched_coin_balance: 0, fetched_coin_balance_block_number: 102) + insert(:unfetched_balance, address_hash: address.hash, block_number: 103) address_hash = address.hash string_address_hash = to_string(address.hash) @@ -200,7 +200,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do %{ id: id, method: "eth_getBalance", - params: [^string_address_hash, "0x66"] + params: [^string_address_hash, "0x67"] } ], _options -> @@ -213,21 +213,21 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", - params: ["0x66", true] + params: ["0x67", true] } ], _ -> - res = eth_block_number_fake_response("0x66") + res = eth_block_number_fake_response("0x67") {:ok, [res]} end) - assert CoinBalanceOnDemand.trigger_fetch(address) == {:pending, 102} + assert CoinBalanceOnDemand.trigger_fetch(address) == {:pending, 103} {:ok, expected_wei} = Wei.cast(2) assert_receive( {:chain_event, :addresses, :on_demand, - [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 102}]} + [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 103}]} ) end end diff --git a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs index b36006b872f6..7e285aae23c6 100644 --- a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs +++ b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs @@ -5,9 +5,12 @@ defmodule Indexer.Fetcher.InternalTransactionTest do import ExUnit.CaptureLog import Mox - alias Explorer.Chain - alias Explorer.Chain.PendingBlockOperation - alias Indexer.Fetcher.{CoinBalance, InternalTransaction, PendingTransaction} + alias Ecto.Multi + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Block, PendingBlockOperation} + alias Explorer.Chain.Import.Runner.Blocks + alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Indexer.Fetcher.{InternalTransaction, PendingTransaction} # MUST use global mode because we aren't guaranteed to get PendingTransactionFetcher's pid back fast enough to `allow` # it to use expectations and stubs from test's pid. @@ -63,7 +66,7 @@ defmodule Indexer.Fetcher.InternalTransactionTest do end end - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) PendingTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) wait_for_results(fn -> @@ -274,7 +277,7 @@ defmodule Indexer.Fetcher.InternalTransactionTest do end end - CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) assert %{block_hash: block_hash} = Repo.get(PendingBlockOperation, block_hash) @@ -466,4 +469,39 @@ defmodule Indexer.Fetcher.InternalTransactionTest do assert logs =~ "foreign_key_violation on internal transactions import, foreign transactions hashes:" end end + + test "doesn't delete pending block operations after block import if no async process was requested", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + fetcher_options = + Keyword.merge([poll: true, json_rpc_named_arguments: json_rpc_named_arguments], InternalTransaction.defaults()) + + if fetcher_options[:poll] do + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> + {:ok, [%{id: id, result: []}]} + end) + end + + InternalTransaction.Supervisor.Case.start_supervised!(fetcher_options) + + %Ecto.Changeset{valid?: true, changes: block_changes} = + Block.changeset(%Block{}, params_for(:block, miner_hash: insert(:address).hash, number: 1)) + + changes_list = [block_changes] + timestamp = DateTime.utc_now() + options = %{timestamps: %{inserted_at: timestamp, updated_at: timestamp}} + + assert [] = Repo.all(PendingBlockOperation) + + {:ok, %{blocks: [%{number: block_number, hash: block_hash}]}} = + Multi.new() + |> Blocks.run(changes_list, options) + |> Repo.transaction() + + assert %{block_number: ^block_number, block_hash: ^block_hash} = Repo.one(PendingBlockOperation) + + Process.sleep(4000) + + assert %{block_number: ^block_number, block_hash: ^block_hash} = Repo.one(PendingBlockOperation) + end end diff --git a/apps/indexer/test/indexer/fetcher/rootstock_data_test.exs b/apps/indexer/test/indexer/fetcher/rootstock_data_test.exs new file mode 100644 index 000000000000..d7954ce886b8 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/rootstock_data_test.exs @@ -0,0 +1,136 @@ +defmodule Indexer.Fetcher.RootstockDataTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + import Mox + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + alias Indexer.Fetcher.RootstockData + + setup :verify_on_exit! + setup :set_mox_global + + if Application.compile_env(:explorer, :chain_type) == "rsk" do + test "do not start when all old blocks are fetched", %{json_rpc_named_arguments: json_rpc_named_arguments} do + RootstockData.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + :timer.sleep(300) + + assert [{Indexer.Fetcher.RootstockData, :undefined, :worker, [Indexer.Fetcher.RootstockData]} | _] = + RootstockData.Supervisor |> Supervisor.which_children() + end + + test "stops when all old blocks are fetched", %{json_rpc_named_arguments: json_rpc_named_arguments} do + block_a = insert(:block) + block_b = insert(:block) + + block_a_number_string = integer_to_quantity(block_a.number) + block_b_number_string = integer_to_quantity(block_b.number) + + EthereumJSONRPC.Mox + |> stub(:json_rpc, fn requests, _options -> + {:ok, + Enum.map(requests, fn + %{id: id, method: "eth_getBlockByNumber", params: [^block_a_number_string, false]} -> + %{ + id: id, + result: %{ + "author" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "difficulty" => "0x6bc767dd80781", + "extraData" => "0x5050594520737061726b706f6f6c2d6574682d7477", + "gasLimit" => "0x7a121d", + "gasUsed" => "0x79cbe9", + "hash" => to_string(block_a.hash), + "logsBloom" => + "0x044d42d008801488400e1809190200a80d06105bc0c4100b047895c0d518327048496108388040140010b8208006288102e206160e21052322440924002090c1c808a0817405ab238086d028211014058e949401012403210314896702d06880c815c3060a0f0809987c81044488292cc11d57882c912a808ca10471c84460460040000c0001012804022000a42106591881d34407420ba401e1c08a8d00a000a34c11821a80222818a4102152c8a0c044032080c6462644223104d618e0e544072008120104408205c60510542264808488220403000106281a0290404220112c10b080145028c8000300b18a2c8280701c882e702210b00410834840108084", + "miner" => to_string(block_a.miner), + "mixHash" => "0xda53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "nonce" => "0x0946e5f01fce12bc", + "number" => block_a_number_string, + "parentHash" => to_string(block_a.parent_hash), + "receiptsRoot" => "0xa7d2b82bd8526de11736c18bd5cc8cfe2692106c4364526f3310ad56d78669c4", + "sealFields" => [ + "0xa0da53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "0x880946e5f01fce12bc" + ], + "sha3Uncles" => "0x483a8a21a5825ad270f358b3ea56e060bbb8b3082d9a92ec8fa17a5c7e6fc1b6", + "size" => "0x544c", + "stateRoot" => "0x85daa9cd528004c1609d4cb3520fd958e85983bb4183124a4a9f7137fd39c691", + "timestamp" => "0x5c8bc76e", + "totalDifficulty" => "0x201a42c35142ae94458", + "transactions" => [], + "transactionsRoot" => "0xcd6c12fa43cd4e92ad5c0bf232b30488bbcbfe273c5b4af0366fced0767d54db", + "uncles" => [], + "withdrawals" => [], + "minimumGasPrice" => "0x0", + "bitcoinMergedMiningHeader" => + "0x00006d20ffd048280094a6ea0851d854036aacaa25ee0f23f0040200000000000000000078d2638fe0b4477c54601e6449051afba8228e0a88ff06b0c91f091fd34d5da57487c76402610517372c2fe9", + "bitcoinMergedMiningCoinbaseTransaction" => + "0x00000000000000805bf0dc9203da49a3b4e3ec913806e43102cc07db991272dc8b7018da57eb5abe59a32d070000ffffffff03449a4d26000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3ad2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a400000000000000000266a24aa21a9ed4ae42ea6dca2687aaed665714bf58b055c4e11f2fb038605930d630b49ad7b9d00000000", + "bitcoinMergedMiningMerkleProof" => + "0x8e5a4ba74eb4eb2f9ad4cabc2913aeed380a5becf7cd4d513341617efb798002bd83a783c31c66a8a8f6cc56c071c2d471cb610e3dc13054b9d216021d8c7e9112f622564449ebedcedf7d4ccb6fe0ffac861b7ed1446c310813cdf712e1e6add28b1fe1c0ae5e916194ba4f285a9340aba41e91bf847bf31acf37a9623a04a2348a37ab9faa5908122db45596bbc03e9c3644b0d4589471c4ff30fc139f3ba50506e9136fa0df799b487494de3e2b3dec937338f1a2e18da057c1f60590a9723672a4355b9914b1d01af9f582d9e856f6e1744be00f268b0b01d559329f7e0685aa63ffeb7c28486d7462292021d1345cddbf7c920ca34bb7aa4c6cdbe068806e35d0db662e7fcda03cb4d779594638c62a1fdd7ec98d1fb6d240d853958abe57561d9b9d0465cf8b9d6ee3c58b0d8b07d6c4c5d8f348e43fe3c06011b6a0008db4e0b16c77ececc3981f9008201cea5939869d648e59a09bd2094b1196ff61126bffb626153deed2563e1745436247c94a85d2947756b606d67633781c99d7", + "hashForMergedMining" => "0xd2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a40" + } + } + + %{id: id, method: "eth_getBlockByNumber", params: [^block_b_number_string, false]} -> + %{ + id: id, + result: %{ + "author" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "difficulty" => "0x6bc767dd80781", + "extraData" => "0x5050594520737061726b706f6f6c2d6574682d7477", + "gasLimit" => "0x7a121d", + "gasUsed" => "0x79cbe9", + "hash" => to_string(block_b.hash), + "logsBloom" => + "0x044d42d008801488400e1809190200a80d06105bc0c4100b047895c0d518327048496108388040140010b8208006288102e206160e21052322440924002090c1c808a0817405ab238086d028211014058e949401012403210314896702d06880c815c3060a0f0809987c81044488292cc11d57882c912a808ca10471c84460460040000c0001012804022000a42106591881d34407420ba401e1c08a8d00a000a34c11821a80222818a4102152c8a0c044032080c6462644223104d618e0e544072008120104408205c60510542264808488220403000106281a0290404220112c10b080145028c8000300b18a2c8280701c882e702210b00410834840108084", + "miner" => to_string(block_b.miner), + "mixHash" => "0xda53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "nonce" => "0x0946e5f01fce12bc", + "number" => block_b_number_string, + "parentHash" => to_string(block_b.parent_hash), + "receiptsRoot" => "0xa7d2b82bd8526de11736c18bd5cc8cfe2692106c4364526f3310ad56d78669c4", + "sealFields" => [ + "0xa0da53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "0x880946e5f01fce12bc" + ], + "sha3Uncles" => "0x483a8a21a5825ad270f358b3ea56e060bbb8b3082d9a92ec8fa17a5c7e6fc1b6", + "size" => "0x544c", + "stateRoot" => "0x85daa9cd528004c1609d4cb3520fd958e85983bb4183124a4a9f7137fd39c691", + "timestamp" => "0x5c8bc76e", + "totalDifficulty" => "0x201a42c35142ae94458", + "transactions" => [], + "transactionsRoot" => "0xcd6c12fa43cd4e92ad5c0bf232b30488bbcbfe273c5b4af0366fced0767d54db", + "uncles" => [], + "withdrawals" => [], + "minimumGasPrice" => "0x1", + "bitcoinMergedMiningHeader" => + "0x00006d20ffd048280094a6ea0851d854036aacaa25ee0f23f0040200000000000000000078d2638fe0b4477c54601e6449051afba8228e0a88ff06b0c91f091fd34d5da57487c76402610517372c2fe9", + "bitcoinMergedMiningCoinbaseTransaction" => + "0x00000000000000805bf0dc9203da49a3b4e3ec913806e43102cc07db991272dc8b7018da57eb5abe59a32d070000ffffffff03449a4d26000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3ad2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a400000000000000000266a24aa21a9ed4ae42ea6dca2687aaed665714bf58b055c4e11f2fb038605930d630b49ad7b9d00000000", + "bitcoinMergedMiningMerkleProof" => + "0x8e5a4ba74eb4eb2f9ad4cabc2913aeed380a5becf7cd4d513341617efb798002bd83a783c31c66a8a8f6cc56c071c2d471cb610e3dc13054b9d216021d8c7e9112f622564449ebedcedf7d4ccb6fe0ffac861b7ed1446c310813cdf712e1e6add28b1fe1c0ae5e916194ba4f285a9340aba41e91bf847bf31acf37a9623a04a2348a37ab9faa5908122db45596bbc03e9c3644b0d4589471c4ff30fc139f3ba50506e9136fa0df799b487494de3e2b3dec937338f1a2e18da057c1f60590a9723672a4355b9914b1d01af9f582d9e856f6e1744be00f268b0b01d559329f7e0685aa63ffeb7c28486d7462292021d1345cddbf7c920ca34bb7aa4c6cdbe068806e35d0db662e7fcda03cb4d779594638c62a1fdd7ec98d1fb6d240d853958abe57561d9b9d0465cf8b9d6ee3c58b0d8b07d6c4c5d8f348e43fe3c06011b6a0008db4e0b16c77ececc3981f9008201cea5939869d648e59a09bd2094b1196ff61126bffb626153deed2563e1745436247c94a85d2947756b606d67633781c99d7", + "hashForMergedMining" => "0xd2508d21d28c8f89d495923c0758ec3f64bd6755b4ec416f5601312600542a40" + } + } + end)} + end) + + pid = RootstockData.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + assert [{Indexer.Fetcher.RootstockData, worker_pid, :worker, [Indexer.Fetcher.RootstockData]} | _] = + RootstockData.Supervisor |> Supervisor.which_children() + + assert is_pid(worker_pid) + + :timer.sleep(300) + + assert [{Indexer.Fetcher.RootstockData, :undefined, :worker, [Indexer.Fetcher.RootstockData]} | _] = + RootstockData.Supervisor |> Supervisor.which_children() + + # Terminates the process so it finishes all Ecto processes. + GenServer.stop(pid) + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/stability/validator_test.exs b/apps/indexer/test/indexer/fetcher/stability/validator_test.exs new file mode 100644 index 000000000000..8aae393a347a --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/stability/validator_test.exs @@ -0,0 +1,270 @@ +defmodule Indexer.Fetcher.Stability.ValidatorTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + import Mox + + alias Explorer.Chain.Stability.Validator, as: ValidatorStability + alias EthereumJSONRPC.Encoder + + setup :verify_on_exit! + setup :set_mox_global + + @accepts_list_of_addresses %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}], + "name" => "getActiveValidatorList", + "inputs" => [%{"type" => "address[]", "name" => "", "internalType" => "address[]"}] + } + + @accepts_integer %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [ + %{ + "internalType" => "uint256", + "name" => "", + "type" => "uint256" + } + ], + "name" => "getActiveValidatorList", + "inputs" => [ + %{ + "internalType" => "uint256", + "name" => "", + "type" => "uint256" + } + ] + } + + if Application.compile_env(:explorer, :chain_type) == "stability" do + describe "check update_validators_list" do + test "deletes absent validators" do + _validator = insert(:validator_stability) + _validator_active = insert(:validator_stability, state: :active) + _validator_inactive = insert(:validator_stability, state: :inactive) + _validator_probation = insert(:validator_stability, state: :probation) + + start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator}) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, 1, fn + [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"] + } + ], + _ -> + <<"0x", _method_id::binary-size(8), result::binary>> = + [@accepts_list_of_addresses] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([[]]) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result + } + ]} + end) + + :timer.sleep(100) + assert ValidatorStability.get_all_validators() == [] + end + + test "updates validators" do + validator_active1 = insert(:validator_stability, state: :active) + validator_active2 = insert(:validator_stability, state: :active) + _validator_active3 = insert(:validator_stability, state: :active) + + validator_inactive1 = insert(:validator_stability, state: :inactive) + validator_inactive2 = insert(:validator_stability, state: :inactive) + _validator_inactive3 = insert(:validator_stability, state: :inactive) + + validator_probation1 = insert(:validator_stability, state: :probation) + validator_probation2 = insert(:validator_stability, state: :probation) + _validator_probation3 = insert(:validator_stability, state: :probation) + + start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator}) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn + [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"] + } + ], + _ -> + <<"0x", _method_id::binary-size(8), result_all::binary>> = + [@accepts_list_of_addresses] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([ + [ + validator_active1.address_hash.bytes, + validator_active2.address_hash.bytes, + validator_inactive1.address_hash.bytes, + validator_inactive2.address_hash.bytes, + validator_probation1.address_hash.bytes, + validator_probation2.address_hash.bytes + ] + ]) + + <<"0x", _method_id::binary-size(8), result_active::binary>> = + [@accepts_list_of_addresses] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([ + [ + validator_active1.address_hash.bytes, + validator_inactive1.address_hash.bytes, + validator_probation1.address_hash.bytes + ] + ]) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_active + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_all + } + ]} + end) + + "0x" <> address_1 = to_string(validator_active1.address_hash) + "0x" <> address_2 = to_string(validator_inactive1.address_hash) + "0x" <> address_3 = to_string(validator_probation1.address_hash) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn + [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x41ee9a53000000000000000000000000" <> ^address_1, + to: "0x0000000000000000000000000000000000000805" + }, + "latest" + ] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x41ee9a53000000000000000000000000" <> ^address_2, + to: "0x0000000000000000000000000000000000000805" + }, + "latest" + ] + }, + %{ + id: id_3, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x41ee9a53000000000000000000000000" <> ^address_3, + to: "0x0000000000000000000000000000000000000805" + }, + "latest" + ] + } + ], + _ -> + <<"0x", _method_id::binary-size(8), result_1::binary>> = + [@accepts_integer] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([10]) + + <<"0x", _method_id::binary-size(8), result_2::binary>> = + [@accepts_integer] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([1]) + + <<"0x", _method_id::binary-size(8), result_3::binary>> = + [@accepts_integer] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([0]) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_1 + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_2 + }, + %{ + id: id_3, + jsonrpc: "2.0", + result: "0x" <> result_3 + } + ]} + end) + + :timer.sleep(100) + validators = ValidatorStability.get_all_validators() + + assert Enum.count(validators) == 6 + + map = + Enum.reduce(validators, %{}, fn validator, map -> Map.put(map, validator.address_hash.bytes, validator) end) + + assert %ValidatorStability{state: :inactive} = map[validator_active2.address_hash.bytes] + assert %ValidatorStability{state: :inactive} = map[validator_inactive2.address_hash.bytes] + assert %ValidatorStability{state: :inactive} = map[validator_probation2.address_hash.bytes] + + assert %ValidatorStability{state: :probation} = map[validator_active1.address_hash.bytes] + assert %ValidatorStability{state: :probation} = map[validator_inactive1.address_hash.bytes] + assert %ValidatorStability{state: :active} = map[validator_probation1.address_hash.bytes] + end + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/token_balance_test.exs index 6c090973fd1c..3da5675d52aa 100644 --- a/apps/indexer/test/indexer/fetcher/token_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_balance_test.exs @@ -261,7 +261,8 @@ defmodule Indexer.Fetcher.TokenBalanceTest do address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", block_number: 19999, token_contract_address_hash: to_string(contract.contract_address_hash), - token_id: 11, + token_id: nil, + value: 100_500, token_type: "ERC-20" }, %{ @@ -275,5 +276,30 @@ defmodule Indexer.Fetcher.TokenBalanceTest do assert TokenBalance.import_token_balances(token_balances_params) == :ok end + + test "import ERC-404 token balances and return :ok" do + contract = insert(:token) + insert(:block, number: 19999) + + token_balances_params = [ + %{ + address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + block_number: 19999, + token_contract_address_hash: to_string(contract.contract_address_hash), + token_id: 11, + token_type: "ERC-404" + }, + %{ + address_hash: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + block_number: 19999, + token_contract_address_hash: to_string(contract.contract_address_hash), + token_id: nil, + value: 100_500, + token_type: "ERC-404" + } + ] + + assert TokenBalance.import_token_balances(token_balances_params) == :ok + end end end diff --git a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs index 8d9987f9de24..bc6fb2dd35ce 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs @@ -50,26 +50,15 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do } """ - abi = - [ - %{ - "type" => "function", - "stateMutability" => "nonpayable", - "payable" => false, - "outputs" => [], - "name" => "tokenURI", - "inputs" => [ - %{"type" => "string", "name" => "name", "internalType" => "string"} - ] - } - ] - |> ABI.parse_specification() - |> Enum.at(0) - encoded_url = - abi - |> Encoder.encode_function_call(["http://localhost:#{bypass.port}/api/card/{id}"]) - |> String.replace("4cf12d26", "") + "0x" <> + (ABI.TypeEncoder.encode(["http://localhost:#{bypass.port}/api/card/{id}"], %ABI.FunctionSelector{ + function: nil, + types: [ + :string + ] + }) + |> Base.encode16(case: :lower)) EthereumJSONRPC.Mox |> expect(:json_rpc, fn [ @@ -174,5 +163,104 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do Application.put_env(:explorer, :http_adapter, HTTPoison) end + + test "re-fetch metadata from baseURI", %{bypass: bypass} do + json = """ + { + "name": "123" + } + """ + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: + "0xc87b56dd0000000000000000000000000000000000000000000000004f3f5ce294ff3d36", + to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" + }, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + error: %{code: -32015, data: "Reverted 0x", message: "execution reverted"}, + id: id, + jsonrpc: "2.0" + } + ]} + end) + + encoded_url = + "0x" <> + (ABI.TypeEncoder.encode(["http://localhost:#{bypass.port}/api/card/"], %ABI.FunctionSelector{ + function: nil, + types: [ + :string + ] + }) + |> Base.encode16(case: :lower)) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x6c0360eb", + to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" + }, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: encoded_url + } + ]} + end) + + Bypass.expect( + bypass, + "GET", + "/api/card/5710384980761197878", + fn conn -> + Conn.resp(conn, 200, json) + end + ) + + insert(:token, + contract_address: build(:address, hash: "0x5caebd3b32e210e85ce3e9d51638b9c445481567"), + type: "ERC-721" + ) + + Application.put_env(:indexer, Indexer.Fetcher.TokenInstance.Helper, base_uri_retry?: true) + + assert [ + {:ok, + %Instance{ + metadata: %{ + "name" => "123" + } + }} + ] = + Helper.batch_fetch_instances([{"0x5caebd3b32e210e85ce3e9d51638b9c445481567", 5_710_384_980_761_197_878}]) + + Application.put_env(:indexer, Indexer.Fetcher.TokenInstance.Helper, base_uri_retry?: false) + end end end diff --git a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs new file mode 100644 index 000000000000..aa1dd230dd4a --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs @@ -0,0 +1,33 @@ +defmodule Indexer.Fetcher.TokenInstance.SanitizeERC1155Test do + use Explorer.DataCase + + alias Explorer.Repo + alias Explorer.Chain.Token.Instance + alias EthereumJSONRPC.Encoder + + describe "sanitizer test" do + test "imports token instances" do + for i <- 0..3 do + token = insert(:token, type: "ERC-1155") + + insert(:address_current_token_balance, + token_type: "ERC-1155", + token_id: i, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + end + + assert [] = Repo.all(Instance) + + start_supervised!({Indexer.Fetcher.TokenInstance.SanitizeERC1155, []}) + + :timer.sleep(500) + + instances = Repo.all(Instance) + + assert Enum.count(instances) == 4 + assert Enum.all?(instances, fn instance -> !is_nil(instance.error) and is_nil(instance.metadata) end) + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs new file mode 100644 index 000000000000..5568b8da3dcc --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs @@ -0,0 +1,39 @@ +defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721Test do + use Explorer.DataCase + + alias Explorer.Repo + alias Explorer.Chain.Token.Instance + alias EthereumJSONRPC.Encoder + + describe "sanitizer test" do + test "imports token instances" do + for x <- 0..3 do + erc_721_token = insert(:token, type: "ERC-721") + + tx = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + address = insert(:address) + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + assert [] = Repo.all(Instance) + + start_supervised!({Indexer.Fetcher.TokenInstance.SanitizeERC721, []}) + + :timer.sleep(500) + + instances = Repo.all(Instance) + + assert Enum.count(instances) == 4 + assert Enum.all?(instances, fn instance -> !is_nil(instance.error) and is_nil(instance.metadata) end) + end + end +end diff --git a/apps/indexer/test/indexer/token_balances_test.exs b/apps/indexer/test/indexer/token_balances_test.exs index 2f5c89c1a6dc..92265544486c 100644 --- a/apps/indexer/test/indexer/token_balances_test.exs +++ b/apps/indexer/test/indexer/token_balances_test.exs @@ -88,6 +88,63 @@ defmodule Indexer.TokenBalancesTest do } = result end + test "fetches balances of ERC-404 tokens" do + address = insert(:address, hash: "0x609991ca0ae39bc4eaf2669976237296d40c2f31") + + address_hash_string = Hash.to_string(address.hash) + + token_contract_address_hash = "0xf7f79032fd395978acb7069c74d21e5a53206559" + + contract_address = insert(:address, hash: token_contract_address_hash) + + token = insert(:token, contract_address: contract_address) + + data = [ + %{ + token_contract_address_hash: Hash.to_string(token.contract_address_hash), + address_hash: address_hash_string, + block_number: 1_000, + token_id: nil, + value: 10, + token_type: "ERC-404" + }, + %{ + token_contract_address_hash: Hash.to_string(token.contract_address_hash), + address_hash: address_hash_string, + block_number: 1_000, + token_id: 5, + token_type: "ERC-404", + value: 2 + } + ] + + get_404_ft_balances_from_blockchain() + get_404_nft_balances_from_blockchain() + + {:ok, result} = TokenBalances.fetch_token_balances_from_blockchain(data) + + assert %{ + failed_token_balances: [], + fetched_token_balances: [ + %{ + value: 10, + token_contract_address_hash: ^token_contract_address_hash, + address_hash: ^address_hash_string, + block_number: 1_000, + value_fetched_at: _ + }, + %{ + token_id: 5, + value: 2, + token_contract_address_hash: ^token_contract_address_hash, + address_hash: ^address_hash_string, + block_number: 1_000, + value_fetched_at: _ + } + ] + } = result + end + test "fetches multiple balances of tokens" do address_1 = insert(:address, hash: "0xecba3c9ea993b0e0594e0b0a0d361a1f9596e310") address_2 = insert(:address, hash: "0x609991ca0ae39bc4eaf2669976237296d40c2f31") @@ -363,6 +420,67 @@ defmodule Indexer.TokenBalancesTest do ) end + defp get_404_ft_balances_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0x70a08231000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f31", + to: "0xf7f79032fd395978acb7069c74d21e5a53206559" + }, + "0x3E8" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000000000000000000000000000000000000000000a" + } + ]} + end + ) + end + + defp get_404_nft_balances_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0x00fdd58e000000000000000000000000609991ca0ae39bc4eaf2669976237296d40c2f310000000000000000000000000000000000000000000000000000000000000005", + to: "0xf7f79032fd395978acb7069c74d21e5a53206559" + }, + "0x3E8" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x0000000000000000000000000000000000000000000000000000000000000002" + } + ]} + end + ) + end + defp get_erc1155_balance_from_blockchain() do expect( EthereumJSONRPC.Mox, diff --git a/apps/indexer/test/indexer/transform/mint_transfers_test.exs b/apps/indexer/test/indexer/transform/mint_transfers_test.exs index 04499a7af454..4670e983a272 100644 --- a/apps/indexer/test/indexer/transform/mint_transfers_test.exs +++ b/apps/indexer/test/indexer/transform/mint_transfers_test.exs @@ -17,8 +17,7 @@ defmodule Indexer.Transform.MintTransfersTest do index: 1, second_topic: "0x0000000000000000000000009a4a90e2732f3fa4087b0bb4bf85c76d14833df1", third_topic: "0x0000000000000000000000007301cfa0e1756b71869e93d4e4dca5c7d0eb0aa6", - transaction_hash: "0x1d5066d30ff3404a9306733136103ac2b0b989951c38df637f464f3667f8d4ee", - type: "mined" + transaction_hash: "0x1d5066d30ff3404a9306733136103ac2b0b989951c38df637f464f3667f8d4ee" } ] @@ -47,8 +46,7 @@ defmodule Indexer.Transform.MintTransfersTest do index: 1, second_topic: "0x0000000000000000000000009a4a90e2732f3fa4087b0bb4bf85c76d14833df1", third_topic: "0x0000000000000000000000007301cfa0e1756b71869e93d4e4dca5c7d0eb0aa6", - transaction_hash: "0x1d5066d30ff3404a9306733136103ac2b0b989951c38df637f464f3667f8d4ee", - type: "mined" + transaction_hash: "0x1d5066d30ff3404a9306733136103ac2b0b989951c38df637f464f3667f8d4ee" } ] diff --git a/apps/indexer/test/indexer/transform/token_transfers_test.exs b/apps/indexer/test/indexer/transform/token_transfers_test.exs index 8833474acccc..f8be8358ab9d 100644 --- a/apps/indexer/test/indexer/transform/token_transfers_test.exs +++ b/apps/indexer/test/indexer/transform/token_transfers_test.exs @@ -19,8 +19,7 @@ defmodule Indexer.Transform.TokenTransfersTest do index: 8, second_topic: "0x000000000000000000000000556813d9cc20acfe8388af029a679d34a63388db", third_topic: "0x00000000000000000000000092148dd870fa1b7c4700f2bd7f44238821c26f73", - transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5", - type: "mined" + transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5" }, %{ address_hash: "0x6ea5ec9cb832e60b6b1654f5826e9be638f276a5", @@ -32,8 +31,7 @@ defmodule Indexer.Transform.TokenTransfersTest do index: 0, second_topic: "0x00000000000000000000000063b0595bb7a0b7edd0549c9557a0c8aee6da667b", third_topic: "0x000000000000000000000000f3089e15d0c23c181d7f98b0878b560bfe193a1d", - transaction_hash: "0x8425a9b81a9bd1c64861110c1a453b84719cb0361d6fa0db68abf7611b9a890e", - type: "mined" + transaction_hash: "0x8425a9b81a9bd1c64861110c1a453b84719cb0361d6fa0db68abf7611b9a890e" }, %{ address_hash: "0x91932e8c6776fb2b04abb71874a7988747728bb2", @@ -45,8 +43,7 @@ defmodule Indexer.Transform.TokenTransfersTest do index: 1, second_topic: "0x0000000000000000000000009851ba177554eb07271ac230a137551e6dd0aa84", third_topic: "0x000000000000000000000000dccb72afee70e60b0c1226288fe86c01b953e8ac", - transaction_hash: "0x4011d9a930a3da620321589a54dc0ca3b88216b4886c7a7c3aaad1fb17702d35", - type: "mined" + transaction_hash: "0x4011d9a930a3da620321589a54dc0ca3b88216b4886c7a7c3aaad1fb17702d35" }, %{ address_hash: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629", @@ -58,8 +55,7 @@ defmodule Indexer.Transform.TokenTransfersTest do third_topic: nil, fourth_topic: nil, index: 1, - transaction_hash: "0x185889bc91372106ecf114a4e23f4ee615e131ae3e698078bd5d2ed7e3f55a49", - type: "mined" + transaction_hash: "0x185889bc91372106ecf114a4e23f4ee615e131ae3e698078bd5d2ed7e3f55a49" }, %{ address_hash: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629", @@ -71,8 +67,7 @@ defmodule Indexer.Transform.TokenTransfersTest do third_topic: nil, fourth_topic: nil, index: 1, - transaction_hash: "0x07510dbfddbac9064f7d607c2d9a14aa26fa19cdfcd578c0b585ff2395df543f", - type: "mined" + transaction_hash: "0x07510dbfddbac9064f7d607c2d9a14aa26fa19cdfcd578c0b585ff2395df543f" } ] @@ -157,8 +152,7 @@ defmodule Indexer.Transform.TokenTransfersTest do second_topic: nil, third_topic: nil, transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", - block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", - type: "mined" + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca" } expected = %{ @@ -193,13 +187,12 @@ defmodule Indexer.Transform.TokenTransfersTest do data: "0x1000000000000c520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", first_topic: "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62", - secon_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", + second_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", third_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", fourth_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", index: 2, transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", - block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", - type: "mined" + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca" } assert TokenTransfers.parse([log]) == %{ @@ -235,13 +228,12 @@ defmodule Indexer.Transform.TokenTransfersTest do data: "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388", first_topic: "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb", - secon_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", + second_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", third_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", fourth_topic: "0x0000000000000000000000006c943470780461b00783ad530a53913bd2c104d3", index: 2, transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", - block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", - type: "mined" + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca" } assert TokenTransfers.parse([log]) == %{ @@ -270,13 +262,12 @@ defmodule Indexer.Transform.TokenTransfersTest do data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", first_topic: "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb", - secon_topic: "0x81D0caF80E9bFfD9bF9c641ab964feB9ef69069e", + second_topic: "0x81D0caF80E9bFfD9bF9c641ab964feB9ef69069e", third_topic: "0x598AF04C88122FA4D1e08C5da3244C39F10D4F14", fourth_topic: "0x0000000000000000000000000000000000000000", index: 6, transaction_hash: "0xa6ad6588edb4abd8ca45f30d2f026ba20b68a3002a5870dbd30cc3752568483b", - block_hash: "0x61b720e40f8c521edd77a52cabce556c18b18b198f78e361f310003386ff1f02", - type: "mined" + block_hash: "0x61b720e40f8c521edd77a52cabce556c18b18b198f78e361f310003386ff1f02" } assert TokenTransfers.parse([log]) == %{ @@ -296,8 +287,7 @@ defmodule Indexer.Transform.TokenTransfersTest do index: 2, second_topic: nil, third_topic: nil, - transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8", - type: "mined" + transaction_hash: "0x6d2dd62c178e55a13b65601f227c4ffdd8aa4e3bcb1f24731363b4f7619e92c8" } error = capture_log(fn -> %{tokens: [], token_transfers: []} = TokenTransfers.parse([log]) end) @@ -319,12 +309,11 @@ defmodule Indexer.Transform.TokenTransfersTest do index: 8, second_topic: "0x000000000000000000000000556813d9cc20acfe8388af029a679d34a63388db", third_topic: "0x00000000000000000000000092148dd870fa1b7c4700f2bd7f44238821c26f73", - transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5", - type: "mined" + transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5" } assert %{ - token_transfers: [%{token_contract_address_hash: ^contract_address_hash, token_type: "ERC-1155"}], + token_transfers: [%{token_contract_address_hash: ^contract_address_hash, token_type: "ERC-20"}], tokens: [%{contract_address_hash: ^contract_address_hash, type: "ERC-1155"}] } = TokenTransfers.parse([log]) end @@ -343,8 +332,7 @@ defmodule Indexer.Transform.TokenTransfersTest do index: 8, second_topic: "0x000000000000000000000000556813d9cc20acfe8388af029a679d34a63388db", third_topic: "0x00000000000000000000000092148dd870fa1b7c4700f2bd7f44238821c26f73", - transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5", - type: "mined" + transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5" }, %{ address_hash: contract_address_hash, @@ -352,24 +340,101 @@ defmodule Indexer.Transform.TokenTransfersTest do data: "0x1000000000000c520000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", first_topic: "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62", - secon_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", + second_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", third_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", fourth_topic: "0x0000000000000000000000009c978f4cfa1fe13406bcc05baf26a35716f881dd", index: 2, transaction_hash: "0x43dfd761974e8c3351d285ab65bee311454eb45b149a015fe7804a33252f19e5", - block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca", - type: "mined" + block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca" } ] assert %{ token_transfers: [ %{token_contract_address_hash: ^contract_address_hash, token_type: "ERC-1155"}, - %{token_contract_address_hash: ^contract_address_hash, token_type: "ERC-1155"} + %{token_contract_address_hash: ^contract_address_hash, token_type: "ERC-20"} ], tokens: [%{contract_address_hash: ^contract_address_hash, type: "ERC-1155"}] } = TokenTransfers.parse(logs) end + + test "parses erc404 token transfer from ERC20Transfer" do + log = %{ + address_hash: "0x03F6CCfCE60273eFbEB9535675C8EFA69D863f37", + block_number: 10_561_358, + data: "0x00000000000000000000000000000000000000000000003635c9adc5de9ffc48", + first_topic: "0xe59fdd36d0d223c0c7d996db7ad796880f45e1936cb0bb7ac102e7082e031487", + second_topic: "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88", + third_topic: "0x00000000000000000000000018336808ed2f2c80795861041f711b299ecd38ca", + fourth_topic: nil, + index: 34, + transaction_hash: "0x6be468f465911ec70103aa83e38c84697848feaf760eee3a181ebcdcab82dc4a", + block_hash: "0x7cffabfd975bded1ec397f44b4af3a97618b96ca0e2f92d70a3025ba233815ca" + } + + assert TokenTransfers.parse([log]) == %{ + token_transfers: [ + %{ + block_hash: "0x7cffabfd975bded1ec397f44b4af3a97618b96ca0e2f92d70a3025ba233815ca", + block_number: 10_561_358, + from_address_hash: "0xc36442b4a4522e871399cd717abdd847ab11fe88", + log_index: 34, + to_address_hash: "0x18336808ed2f2c80795861041f711b299ecd38ca", + token_contract_address_hash: "0x03F6CCfCE60273eFbEB9535675C8EFA69D863f37", + amounts: [ + 999_999_999_999_999_999_048 + ], + token_ids: [], + token_type: "ERC-404", + transaction_hash: "0x6be468f465911ec70103aa83e38c84697848feaf760eee3a181ebcdcab82dc4a" + } + ], + tokens: [ + %{ + contract_address_hash: "0x03F6CCfCE60273eFbEB9535675C8EFA69D863f37", + type: "ERC-404" + } + ] + } + end + + test "parses erc404 token transfer from ERC721Transfer" do + log = %{ + address_hash: "0x68995c84aFb019913942E53F27E7ceA47D86Cd9d", + block_number: 10_514_498, + data: "0x", + first_topic: "0xe5f815dc84b8cecdfd4beedfc3f91ab5be7af100eca4e8fb11552b867995394f", + second_topic: "0x000000000000000000000000fd7ec4d8b6ba1a72f3895b6ce3846b00d6b83aab", + third_topic: "0x0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d", + fourth_topic: "0x000000000000000000000000000000000000000000000000000000000000000a", + index: 41, + transaction_hash: "0xe201aed9c948f46395c6acc54de5e9c3ebe0c41a5c34cc6a507b67ec46057c55", + block_hash: "0xea065ff2fc04177bbef27317209a25f2633199aa453b86ee405b619c495b2e77" + } + + assert TokenTransfers.parse([log]) == %{ + token_transfers: [ + %{ + block_hash: "0xea065ff2fc04177bbef27317209a25f2633199aa453b86ee405b619c495b2e77", + block_number: 10_514_498, + from_address_hash: "0xfd7ec4d8b6ba1a72f3895b6ce3846b00d6b83aab", + log_index: 41, + to_address_hash: "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", + token_contract_address_hash: "0x68995c84aFb019913942E53F27E7ceA47D86Cd9d", + amounts: [], + token_ids: [10], + token_type: "ERC-404", + transaction_hash: "0xe201aed9c948f46395c6acc54de5e9c3ebe0c41a5c34cc6a507b67ec46057c55" + } + ], + tokens: [ + %{ + contract_address_hash: "0x68995c84aFb019913942E53F27E7ceA47D86Cd9d", + type: "ERC-404" + } + ] + } + end end defp truncated_hash("0x000000000000000000000000" <> rest) do diff --git a/apps/indexer/test/support/indexer/fetcher/beacon_blob_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/beacon_blob_supervisor_case.ex new file mode 100644 index 000000000000..661b7ce3133d --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/beacon_blob_supervisor_case.ex @@ -0,0 +1,18 @@ +defmodule Indexer.Fetcher.Beacon.Blob.Supervisor.Case do + alias Indexer.Fetcher.Beacon.Blob + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + merged_fetcher_arguments = + Keyword.merge( + fetcher_arguments, + flush_interval: 50, + max_batch_size: 1, + max_concurrency: 1, + poll: false + ) + + [merged_fetcher_arguments] + |> Blob.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/coin_balance_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/coin_balance_catchup_supervisor_case.ex similarity index 69% rename from apps/indexer/test/support/indexer/fetcher/coin_balance_supervisor_case.ex rename to apps/indexer/test/support/indexer/fetcher/coin_balance_catchup_supervisor_case.ex index 6e2a9e22afc9..17831099c327 100644 --- a/apps/indexer/test/support/indexer/fetcher/coin_balance_supervisor_case.ex +++ b/apps/indexer/test/support/indexer/fetcher/coin_balance_catchup_supervisor_case.ex @@ -1,5 +1,5 @@ -defmodule Indexer.Fetcher.CoinBalance.Supervisor.Case do - alias Indexer.Fetcher.CoinBalance +defmodule Indexer.Fetcher.CoinBalance.Catchup.Supervisor.Case do + alias Indexer.Fetcher.CoinBalance.Catchup def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do merged_fetcher_arguments = @@ -11,7 +11,7 @@ defmodule Indexer.Fetcher.CoinBalance.Supervisor.Case do ) [merged_fetcher_arguments] - |> CoinBalance.Supervisor.child_spec() + |> Catchup.Supervisor.child_spec() |> ExUnit.Callbacks.start_supervised!() end end diff --git a/apps/indexer/test/support/indexer/fetcher/coin_balance_realtime_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/coin_balance_realtime_supervisor_case.ex new file mode 100644 index 000000000000..878f6ea7f3f0 --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/coin_balance_realtime_supervisor_case.ex @@ -0,0 +1,17 @@ +defmodule Indexer.Fetcher.CoinBalance.Realtime.Supervisor.Case do + alias Indexer.Fetcher.CoinBalance.Realtime + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + merged_fetcher_arguments = + Keyword.merge( + fetcher_arguments, + flush_interval: 50, + max_batch_size: 1, + max_concurrency: 1 + ) + + [merged_fetcher_arguments] + |> Realtime.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/internal_transaction_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/internal_transaction_supervisor_case.ex index b067997b5fbf..0c3ba2be7358 100644 --- a/apps/indexer/test/support/indexer/fetcher/internal_transaction_supervisor_case.ex +++ b/apps/indexer/test/support/indexer/fetcher/internal_transaction_supervisor_case.ex @@ -4,11 +4,13 @@ defmodule Indexer.Fetcher.InternalTransaction.Supervisor.Case do def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do merged_fetcher_arguments = Keyword.merge( - fetcher_arguments, - flush_interval: 50, - max_batch_size: 1, - max_concurrency: 1, - poll: false + [ + flush_interval: 50, + max_batch_size: 1, + max_concurrency: 1, + poll: false + ], + fetcher_arguments ) [merged_fetcher_arguments] diff --git a/apps/indexer/test/support/indexer/fetcher/rootstock_data_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/rootstock_data_supervisor_case.ex new file mode 100644 index 000000000000..d58f3c5f10cd --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/rootstock_data_supervisor_case.ex @@ -0,0 +1,17 @@ +defmodule Indexer.Fetcher.RootstockData.Supervisor.Case do + alias Indexer.Fetcher.RootstockData + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + merged_fetcher_arguments = + Keyword.merge( + fetcher_arguments, + interval: 1, + batch_size: 1, + max_concurrency: 1 + ) + + [merged_fetcher_arguments] + |> RootstockData.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/bin/install_chrome_headless.sh b/bin/install_chrome_headless.sh index 731ee458c8be..63f2d98a2493 100755 --- a/bin/install_chrome_headless.sh +++ b/bin/install_chrome_headless.sh @@ -2,8 +2,8 @@ export DISPLAY=:99.0 sh -e /etc/init.d/xvfb start export CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json" | jq -r '.channels' | jq -r '.Stable' | jq -r '.version') -curl -L -O "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROMEDRIVER_VERSION}/linux64/chrome-linux64.zip" -unzip chromedriver_linux64.zip +curl -L -O "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip" +unzip -j chromedriver-linux64.zip sudo chmod +x chromedriver sudo mv chromedriver /usr/local/bin wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb diff --git a/config/config.exs b/config/config.exs index 554d3ae557b1..f09d21a3f065 100644 --- a/config/config.exs +++ b/config/config.exs @@ -38,9 +38,17 @@ config :logger, {LoggerFileBackend, :api}, {LoggerFileBackend, :block_import_timings}, {LoggerFileBackend, :account}, - {LoggerFileBackend, :api_v2} + {LoggerFileBackend, :api_v2}, + LoggerJSON ] +config :logger_json, :backend, + metadata: + ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count + block_number step count error_count shrunk import_id transaction_id duration status unit endpoint method)a, + json_encoder: Jason, + formatter: LoggerJSON.Formatters.BasicLogger + config :logger, :console, # Use same format for all loggers, even though the level should only ever be `:error` for `:error` backend format: "$dateT$time $metadata[$level] $message\n", diff --git a/config/config_helper.exs b/config/config_helper.exs index af4e8de6aefe..a703ac522bd1 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -7,12 +7,25 @@ defmodule ConfigHelper do def repos do base_repos = [Explorer.Repo, Explorer.Repo.Account] - case System.get_env("CHAIN_TYPE") do - "polygon_edge" -> base_repos ++ [Explorer.Repo.PolygonEdge] - "polygon_zkevm" -> base_repos ++ [Explorer.Repo.PolygonZkevm] - "rsk" -> base_repos ++ [Explorer.Repo.RSK] - "suave" -> base_repos ++ [Explorer.Repo.Suave] - _ -> base_repos + repos = + case System.get_env("CHAIN_TYPE") do + "ethereum" -> base_repos ++ [Explorer.Repo.Beacon] + "optimism" -> base_repos ++ [Explorer.Repo.Optimism] + "polygon_edge" -> base_repos ++ [Explorer.Repo.PolygonEdge] + "polygon_zkevm" -> base_repos ++ [Explorer.Repo.PolygonZkevm] + "rsk" -> base_repos ++ [Explorer.Repo.RSK] + "shibarium" -> base_repos ++ [Explorer.Repo.Shibarium] + "suave" -> base_repos ++ [Explorer.Repo.Suave] + "filecoin" -> base_repos ++ [Explorer.Repo.Filecoin] + "stability" -> base_repos ++ [Explorer.Repo.Stability] + "zksync" -> base_repos ++ [Explorer.Repo.ZkSync] + _ -> base_repos + end + + if System.get_env("BRIDGED_TOKENS_ENABLED") do + repos ++ [Explorer.Repo.BridgedTokens] + else + repos end end @@ -49,6 +62,17 @@ defmodule ConfigHelper do end end + @spec parse_float_env_var(String.t(), float()) :: float() + def parse_float_env_var(env_var, default_value) do + env_var + |> safe_get_env(to_string(default_value)) + |> Float.parse() + |> case do + {float, _} -> float + _ -> 0 + end + end + @spec parse_integer_or_nil_env_var(String.t()) :: non_neg_integer() | nil def parse_integer_or_nil_env_var(env_var) do env_var @@ -146,6 +170,20 @@ defmodule ConfigHelper do end end + @spec exchange_rates_secondary_coin_price_source() :: Price.CoinGecko | Price.CoinMarketCap | Price.CryptoCompare + def exchange_rates_secondary_coin_price_source do + cmc_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINMARKETCAP_SECONDARY_COIN_ID") + cg_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINGECKO_SECONDARY_COIN_ID") + cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL") + + cond do + cg_secondary_coin_id && cg_secondary_coin_id !== "" -> Price.CoinGecko + cmc_secondary_coin_id && cmc_secondary_coin_id !== "" -> Price.CoinMarketCap + cc_secondary_coin_symbol && cc_secondary_coin_symbol !== "" -> Price.CryptoCompare + true -> Price.CryptoCompare + end + end + def block_transformer do block_transformers = %{ "clique" => Blocks.Clique, @@ -179,4 +217,12 @@ defmodule ConfigHelper do rescue err -> raise "Invalid JSON in environment variable #{env_var}: #{inspect(err)}" end + + @spec chain_type() :: String.t() + def chain_type, do: System.get_env("CHAIN_TYPE") || "default" + + @spec eth_call_url(String.t() | nil) :: String.t() | nil + def eth_call_url(default \\ nil) do + System.get_env("ETHEREUM_JSONRPC_ETH_CALL_URL") || System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || default + end end diff --git a/config/dev.exs b/config/dev.exs index 323212e107f7..27154824a939 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -3,6 +3,8 @@ import Config # DO NOT make it `:debug` or all Ecto logs will be shown for indexer config :logger, :console, level: :info +config :logger_json, :backend, level: :none + config :logger, :ecto, level: :debug, path: Path.absname("logs/dev/ecto.log") diff --git a/config/prod.exs b/config/prod.exs index 1d9be54e3fc6..2e8c9db24d97 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -2,7 +2,9 @@ import Config # Do not print debug messages in production -config :logger, :console, level: :info +config :logger, :console, level: :none + +config :logger_json, :backend, level: :info config :logger, :ecto, level: :info, diff --git a/config/runtime.exs b/config/runtime.exs index 6332d618d0c8..b1f384bdba1b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -4,8 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -chain_type = System.get_env("CHAIN_TYPE") || "ethereum" - ###################### ### BlockScout Web ### ###################### @@ -105,7 +103,8 @@ config :block_scout_web, :api_rate_limit, whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS"), is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"), api_v2_ui_limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 5), - api_v2_token_ttl_seconds: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS", 18000) + api_v2_token_ttl_seconds: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS", 18000), + eth_json_rpc_max_batch_size: ConfigHelper.parse_integer_env_var("ETH_JSON_RPC_MAX_BATCH_SIZE", 5) # Configures History price_chart_config = @@ -132,7 +131,11 @@ config :block_scout_web, :chart, config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance, coin_balance_history_days: ConfigHelper.parse_integer_env_var("COIN_BALANCE_HISTORY_DAYS", 10) -config :block_scout_web, BlockScoutWeb.API.V2, enabled: ConfigHelper.parse_bool_env_var("API_V2_ENABLED") +config :block_scout_web, BlockScoutWeb.API.V2, enabled: ConfigHelper.parse_bool_env_var("API_V2_ENABLED", "true") + +config :block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + service_url: System.get_env("MICROSERVICE_TRANSACTION_INTERPRETATION_URL"), + enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TRANSACTION_INTERPRETATION_ENABLED") # Configures Ueberauth's Auth0 auth provider config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, @@ -160,9 +163,10 @@ config :ethereum_jsonrpc, EthereumJSONRPC.HTTP, |> Map.to_list() config :ethereum_jsonrpc, EthereumJSONRPC.Geth, + block_traceable?: ConfigHelper.parse_bool_env_var("ETHEREUM_JSONRPC_GETH_TRACE_BY_BLOCK"), debug_trace_transaction_timeout: System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT", "5s"), tracer: - if(chain_type == "polygon_edge", + if(ConfigHelper.chain_type() == "polygon_edge", do: "polygon_edge", else: System.get_env("INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE", "call_tracer") ) @@ -185,15 +189,14 @@ checksum_function = System.get_env("CHECKSUM_FUNCTION") exchange_rates_coin = System.get_env("EXCHANGE_RATES_COIN") config :explorer, - chain_type: chain_type, coin: System.get_env("COIN") || exchange_rates_coin || "ETH", coin_name: System.get_env("COIN_NAME") || exchange_rates_coin || "ETH", allowed_solidity_evm_versions: System.get_env("CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS") || - "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,default", + "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,default", allowed_vyper_evm_versions: System.get_env("CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS") || - "byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,default", + "byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default", include_uncles_in_average_block_time: ConfigHelper.parse_bool_env_var("UNCLES_IN_AVERAGE_BLOCK_TIME"), healthy_blocks_period: ConfigHelper.parse_time_env_var("HEALTHY_BLOCKS_PERIOD", "5m"), realtime_events_sender: @@ -201,14 +204,18 @@ config :explorer, do: Explorer.Chain.Events.DBSender, else: Explorer.Chain.Events.SimpleSender ), - enable_caching_implementation_data_of_proxy: true, - avg_block_time_as_ttl_cached_implementation_data_of_proxy: true, - fallback_ttl_cached_implementation_data_of_proxy: :timer.seconds(4), - implementation_data_fetching_timeout: :timer.seconds(2), restricted_list: System.get_env("RESTRICTED_LIST"), restricted_list_key: System.get_env("RESTRICTED_LIST_KEY"), checksum_function: checksum_function && String.to_atom(checksum_function), - elasticity_multiplier: ConfigHelper.parse_integer_env_var("EIP_1559_ELASTICITY_MULTIPLIER", 2) + elasticity_multiplier: ConfigHelper.parse_integer_env_var("EIP_1559_ELASTICITY_MULTIPLIER", 2), + base_fee_max_change_denominator: ConfigHelper.parse_integer_env_var("EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR", 8) + +config :explorer, :proxy, + caching_implementation_data_enabled: true, + implementation_data_ttl_via_avg_block_time: + ConfigHelper.parse_bool_env_var("CONTRACT_PROXY_IMPLEMENTATION_TTL_VIA_AVG_BLOCK_TIME", "true"), + fallback_cached_implementation_data_ttl: :timer.seconds(4), + implementation_data_fetching_timeout: :timer.seconds(2) config :explorer, Explorer.Chain.Events.Listener, enabled: @@ -242,16 +249,22 @@ config :explorer, Explorer.Chain.Cache.PendingBlockOperation, config :explorer, Explorer.Chain.Cache.GasPriceOracle, global_ttl: ConfigHelper.parse_time_env_var("GAS_PRICE_ORACLE_CACHE_PERIOD", "30s"), + simple_transaction_gas: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SIMPLE_TRANSACTION_GAS", 21000), num_of_blocks: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_NUM_OF_BLOCKS", 200), safelow_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SAFELOW_PERCENTILE", 35), average_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_AVERAGE_PERCENTILE", 60), - fast_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_FAST_PERCENTILE", 90) + fast_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_FAST_PERCENTILE", 90), + safelow_time_coefficient: ConfigHelper.parse_float_env_var("GAS_PRICE_ORACLE_SAFELOW_TIME_COEFFICIENT", 5), + average_time_coefficient: ConfigHelper.parse_float_env_var("GAS_PRICE_ORACLE_AVERAGE_TIME_COEFFICIENT", 3), + fast_time_coefficient: ConfigHelper.parse_float_env_var("GAS_PRICE_ORACLE_FAST_TIME_COEFFICIENT", 1) config :explorer, Explorer.Chain.Cache.RootstockLockedBTC, enabled: System.get_env("ETHEREUM_JSONRPC_VARIANT") == "rsk", global_ttl: ConfigHelper.parse_time_env_var("ROOTSTOCK_LOCKED_BTC_CACHE_PERIOD", "10m"), locking_cap: ConfigHelper.parse_integer_env_var("ROOTSTOCK_LOCKING_CAP", 21_000_000) +config :explorer, Explorer.Chain.Cache.OptimismFinalizationPeriod, enabled: ConfigHelper.chain_type() == "optimism" + config :explorer, Explorer.Counters.AddressTransactionsGasUsageCounter, cache_period: ConfigHelper.parse_time_env_var("CACHE_ADDRESS_TRANSACTIONS_GAS_USAGE_COUNTER_PERIOD", "30m") @@ -278,6 +291,21 @@ config :explorer, Explorer.Counters.AddressTokenUsdSum, config :explorer, Explorer.Counters.AddressTokenTransfersCounter, cache_period: ConfigHelper.parse_time_env_var("CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD", "1h") +config :explorer, Explorer.Counters.LastOutputRootSizeCounter, + enabled: ConfigHelper.chain_type() == "optimism", + enable_consolidation: ConfigHelper.chain_type() == "optimism", + cache_period: ConfigHelper.parse_time_env_var("CACHE_OPTIMISM_LAST_OUTPUT_ROOT_SIZE_COUNTER_PERIOD", "5m") + +config :explorer, Explorer.Counters.Transactions24hStats, + enabled: true, + cache_period: ConfigHelper.parse_time_env_var("CACHE_TRANSACTIONS_24H_STATS_PERIOD", "1h"), + enable_consolidation: true + +config :explorer, Explorer.Counters.FreshPendingTransactionsCounter, + enabled: true, + cache_period: ConfigHelper.parse_time_env_var("CACHE_FRESH_PENDING_TRANSACTIONS_COUNTER_PERIOD", "5m"), + enable_consolidation: true + config :explorer, Explorer.ExchangeRates, store: :ets, enabled: !disable_exchange_rates?, @@ -286,27 +314,41 @@ config :explorer, Explorer.ExchangeRates, config :explorer, Explorer.ExchangeRates.Source, source: ConfigHelper.exchange_rates_source(), price_source: ConfigHelper.exchange_rates_price_source(), + secondary_coin_price_source: ConfigHelper.exchange_rates_secondary_coin_price_source(), market_cap_source: ConfigHelper.exchange_rates_market_cap_source(), tvl_source: ConfigHelper.exchange_rates_tvl_source() +cmc_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINMARKETCAP_SECONDARY_COIN_ID") + config :explorer, Explorer.ExchangeRates.Source.CoinMarketCap, api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"), - coin_id: System.get_env("EXCHANGE_RATES_COINMARKETCAP_COIN_ID") + coin_id: System.get_env("EXCHANGE_RATES_COINMARKETCAP_COIN_ID"), + secondary_coin_id: cmc_secondary_coin_id + +cg_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINGECKO_SECONDARY_COIN_ID") config :explorer, Explorer.ExchangeRates.Source.CoinGecko, platform: System.get_env("EXCHANGE_RATES_COINGECKO_PLATFORM_ID"), api_key: System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"), - coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID") + coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"), + secondary_coin_id: cg_secondary_coin_id config :explorer, Explorer.ExchangeRates.Source.DefiLlama, coin_id: System.get_env("EXCHANGE_RATES_DEFILLAMA_COIN_ID") +cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL") + +config :explorer, Explorer.Market.History.Source.Price.CryptoCompare, secondary_coin_symbol: cc_secondary_coin_symbol + config :explorer, Explorer.ExchangeRates.TokenExchangeRates, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_TOKEN_EXCHANGE_RATE", "true"), interval: ConfigHelper.parse_time_env_var("TOKEN_EXCHANGE_RATE_INTERVAL", "5s"), refetch_interval: ConfigHelper.parse_time_env_var("TOKEN_EXCHANGE_RATE_REFETCH_INTERVAL", "1h"), max_batch_size: ConfigHelper.parse_integer_env_var("TOKEN_EXCHANGE_RATE_MAX_BATCH_SIZE", 150) -config :explorer, Explorer.Market.History.Cataloger, enabled: !disable_indexer? && !disable_exchange_rates? +config :explorer, Explorer.Market.History.Cataloger, + enabled: !disable_indexer? && !disable_exchange_rates?, + history_fetch_interval: ConfigHelper.parse_time_env_var("MARKET_HISTORY_FETCH_INTERVAL", "1h"), + secondary_coin_enabled: cmc_secondary_coin_id || cg_secondary_coin_id || cc_secondary_coin_symbol config :explorer, Explorer.Chain.Transaction, suave_bid_contracts: System.get_env("SUAVE_BID_CONTRACTS", "") @@ -371,6 +413,15 @@ config :explorer, Explorer.ThirdPartyIntegrations.Sourcify, chain_id: System.get_env("CHAIN_ID"), repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts" +config :explorer, Explorer.ThirdPartyIntegrations.SolidityScan, + chain_id: System.get_env("SOLIDITYSCAN_CHAIN_ID"), + api_key: System.get_env("SOLIDITYSCAN_API_TOKEN") + +config :explorer, Explorer.ThirdPartyIntegrations.NovesFi, + service_url: System.get_env("NOVES_FI_BASE_API_URL") || "https://blockscout.noves.fi", + chain_name: System.get_env("NOVES_FI_CHAIN_NAME"), + api_key: System.get_env("NOVES_FI_API_TOKEN") + enabled? = ConfigHelper.parse_bool_env_var("MICROSERVICE_SC_VERIFIER_ENABLED") # or "eth_bytecode_db" type = System.get_env("MICROSERVICE_SC_VERIFIER_TYPE", "sc_verifier") @@ -379,7 +430,8 @@ config :explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, service_url: System.get_env("MICROSERVICE_SC_VERIFIER_URL") || "https://eth-bytecode-db.services.blockscout.com/", enabled: enabled?, type: type, - eth_bytecode_db?: enabled? && type == "eth_bytecode_db" + eth_bytecode_db?: enabled? && type == "eth_bytecode_db", + api_key: System.get_env("MICROSERVICE_SC_VERIFIER_API_KEY") config :explorer, Explorer.Visualize.Sol2uml, service_url: System.get_env("MICROSERVICE_VISUALIZE_SOL2UML_URL"), @@ -389,10 +441,28 @@ config :explorer, Explorer.SmartContract.SigProviderInterface, service_url: System.get_env("MICROSERVICE_SIG_PROVIDER_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_SIG_PROVIDER_ENABLED") -config :explorer, Explorer.ThirdPartyIntegrations.AirTable, +config :explorer, Explorer.MicroserviceInterfaces.BENS, + service_url: System.get_env("MICROSERVICE_BENS_URL"), + enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_BENS_ENABLED") + +config :explorer, Explorer.MicroserviceInterfaces.AccountAbstraction, + service_url: System.get_env("MICROSERVICE_ACCOUNT_ABSTRACTION_URL"), + enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_ACCOUNT_ABSTRACTION_ENABLED") + +config :explorer, :air_table_public_tags, table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") +audit_reports_table_url = System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_URL") + +audit_reports_api_key = + System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY") || System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") + +config :explorer, :air_table_audit_reports, + table_url: audit_reports_table_url, + api_key: audit_reports_api_key, + enabled: (audit_reports_table_url && audit_reports_api_key && true) || false + config :explorer, Explorer.Mailer, adapter: Bamboo.SendGridAdapter, api_key: System.get_env("ACCOUNT_SENDGRID_API_KEY") @@ -405,7 +475,9 @@ config :explorer, Explorer.Account, ], resend_interval: ConfigHelper.parse_time_env_var("ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL", "5m"), private_tags_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_PRIVATE_TAGS_LIMIT", 2000), - watchlist_addresses_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_ADDRESSES_LIMIT", 15) + watchlist_addresses_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_ADDRESSES_LIMIT", 15), + notifications_limit_for_30_days: + ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS", 1000) config :explorer, :token_id_migration, first_block: ConfigHelper.parse_integer_env_var("TOKEN_ID_MIGRATION_FIRST_BLOCK", 0), @@ -433,7 +505,8 @@ config :explorer, Explorer.Chain.Cache.MinMissingBlockNumber, config :explorer, Explorer.TokenInstanceOwnerAddressMigration, concurrency: ConfigHelper.parse_integer_env_var("TOKEN_INSTANCE_OWNER_MIGRATION_CONCURRENCY", 5), - batch_size: ConfigHelper.parse_integer_env_var("TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE", 50) + batch_size: ConfigHelper.parse_integer_env_var("TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE", 50), + enabled: ConfigHelper.parse_bool_env_var("TOKEN_INSTANCE_OWNER_MIGRATION_ENABLED") config :explorer, Explorer.Chain.Transaction, rootstock_remasc_address: System.get_env("ROOTSTOCK_REMASC_ADDRESS"), @@ -442,18 +515,47 @@ config :explorer, Explorer.Chain.Transaction, config :explorer, Explorer.Chain.Cache.AddressesTabsCounters, ttl: ConfigHelper.parse_time_env_var("ADDRESSES_TABS_COUNTERS_TTL", "10m") +config :explorer, Explorer.Migrator.TransactionsDenormalization, + batch_size: ConfigHelper.parse_integer_env_var("DENORMALIZATION_MIGRATION_BATCH_SIZE", 500), + concurrency: ConfigHelper.parse_integer_env_var("DENORMALIZATION_MIGRATION_CONCURRENCY", 10) + +config :explorer, Explorer.Migrator.TokenTransferTokenType, + batch_size: ConfigHelper.parse_integer_env_var("TOKEN_TRANSFER_TOKEN_TYPE_MIGRATION_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("TOKEN_TRANSFER_TOKEN_TYPE_MIGRATION_CONCURRENCY", 1) + +config :explorer, Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers, + batch_size: ConfigHelper.parse_integer_env_var("SANITIZE_INCORRECT_NFT_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("SANITIZE_INCORRECT_NFT_CONCURRENCY", 1) + +config :explorer, Explorer.Chain.BridgedToken, + eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), + bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), + poa_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_POA_OMNI_BRIDGE_MEDIATOR"), + amb_bridge_mediators: System.get_env("BRIDGED_TOKENS_AMB_BRIDGE_MEDIATORS"), + foreign_json_rpc: System.get_env("BRIDGED_TOKENS_FOREIGN_JSON_RPC", "") + ############### ### Indexer ### ############### +trace_first_block = ConfigHelper.parse_integer_env_var("TRACE_FIRST_BLOCK", 0) +trace_last_block = ConfigHelper.parse_integer_or_nil_env_var("TRACE_LAST_BLOCK") + +trace_block_ranges = + case ConfigHelper.safe_get_env("TRACE_BLOCK_RANGES", nil) do + "" -> "#{trace_first_block}..#{trace_last_block || "latest"}" + ranges -> ranges + end + config :indexer, block_transformer: ConfigHelper.block_transformer(), metadata_updater_milliseconds_interval: ConfigHelper.parse_time_env_var("TOKEN_METADATA_UPDATE_INTERVAL", "48h"), block_ranges: System.get_env("BLOCK_RANGES"), first_block: ConfigHelper.parse_integer_env_var("FIRST_BLOCK", 0), last_block: ConfigHelper.parse_integer_or_nil_env_var("LAST_BLOCK"), - trace_first_block: ConfigHelper.parse_integer_env_var("TRACE_FIRST_BLOCK", 0), - trace_last_block: ConfigHelper.parse_integer_or_nil_env_var("TRACE_LAST_BLOCK"), + trace_block_ranges: trace_block_ranges, + trace_first_block: trace_first_block, + trace_last_block: trace_last_block, fetch_rewards_way: System.get_env("FETCH_REWARDS_WAY", "trace_block"), memory_limit: ConfigHelper.indexer_memory_limit(), receipts_batch_size: ConfigHelper.parse_integer_env_var("INDEXER_RECEIPTS_BATCH_SIZE", 250), @@ -488,9 +590,7 @@ config :indexer, Indexer.Fetcher.TransactionAction, ) config :indexer, Indexer.Fetcher.PendingTransaction.Supervisor, - disabled?: - System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu" || - ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER") + disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER") config :indexer, Indexer.Fetcher.Token, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_CONCURRENCY", 10) @@ -512,8 +612,11 @@ config :indexer, Indexer.Fetcher.BlockReward.Supervisor, config :indexer, Indexer.Fetcher.InternalTransaction.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER") -config :indexer, Indexer.Fetcher.CoinBalance.Supervisor, - disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER") +disable_coin_balances_fetcher? = ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER") + +config :indexer, Indexer.Fetcher.CoinBalance.Catchup.Supervisor, disabled?: disable_coin_balances_fetcher? + +config :indexer, Indexer.Fetcher.CoinBalance.Realtime.Supervisor, disabled?: disable_coin_balances_fetcher? config :indexer, Indexer.Fetcher.TokenUpdater.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER") @@ -524,6 +627,8 @@ config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor, config :indexer, Indexer.Block.Realtime.Supervisor, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_REALTIME_INDEXER") +config :indexer, Indexer.Block.Catchup.Supervisor, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_CATCHUP_INDEXER") + config :indexer, Indexer.Fetcher.TokenInstance.Realtime.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_REALTIME_FETCHER") @@ -533,8 +638,14 @@ config :indexer, Indexer.Fetcher.TokenInstance.Retry.Supervisor, config :indexer, Indexer.Fetcher.TokenInstance.Sanitize.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_SANITIZE_FETCHER") -config :indexer, Indexer.Fetcher.TokenInstance.LegacySanitize.Supervisor, - disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_LEGACY_SANITIZE_FETCHER", "true") +config :indexer, Indexer.Fetcher.TokenInstance.LegacySanitize, + enabled: !ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_LEGACY_SANITIZE_FETCHER", "true") + +config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC1155, + enabled: !ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_ERC_1155_SANITIZE_FETCHER", "false") + +config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, + enabled: !ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_ERC_721_SANITIZE_FETCHER", "false") config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE", 100), @@ -554,6 +665,9 @@ config :indexer, Indexer.Fetcher.BlockReward, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_BLOCK_REWARD_BATCH_SIZE", 10), concurrency: ConfigHelper.parse_integer_env_var("INDEXER_BLOCK_REWARD_CONCURRENCY", 4) +config :indexer, Indexer.Fetcher.TokenInstance.Helper, + base_uri_retry?: ConfigHelper.parse_bool_env_var("INDEXER_TOKEN_INSTANCE_USE_BASE_URI_RETRY") + config :indexer, Indexer.Fetcher.TokenInstance.Retry, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_RETRY_CONCURRENCY", 10), batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_RETRY_BATCH_SIZE", 10), @@ -568,33 +682,83 @@ config :indexer, Indexer.Fetcher.TokenInstance.Sanitize, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_SANITIZE_BATCH_SIZE", 10) config :indexer, Indexer.Fetcher.TokenInstance.LegacySanitize, - concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_CONCURRENCY", 10), + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_CONCURRENCY", 2), batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_BATCH_SIZE", 10) +config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC1155, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_ERC_1155_SANITIZE_CONCURRENCY", 2), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_ERC_1155_SANITIZE_BATCH_SIZE", 10) + +config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_ERC_721_SANITIZE_CONCURRENCY", 2), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_ERC_721_SANITIZE_BATCH_SIZE", 10), + tokens_queue_size: + ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_INSTANCE_ERC_721_SANITIZE_TOKENS_BATCH_SIZE", 100) + config :indexer, Indexer.Fetcher.InternalTransaction, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE", 10), concurrency: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY", 4), indexing_finished_threshold: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_INDEXING_FINISHED_THRESHOLD", 1000) -config :indexer, Indexer.Fetcher.CoinBalance, - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_BATCH_SIZE", 500), - concurrency: ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_CONCURRENCY", 4) +coin_balances_batch_size = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_BATCH_SIZE", 100) +coin_balances_concurrency = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_CONCURRENCY", 4) + +config :indexer, Indexer.Fetcher.CoinBalance.Catchup, + batch_size: coin_balances_batch_size, + concurrency: coin_balances_concurrency + +config :indexer, Indexer.Fetcher.CoinBalance.Realtime, + batch_size: coin_balances_batch_size, + concurrency: coin_balances_concurrency + +config :indexer, Indexer.Fetcher.Optimism.TxnBatch.Supervisor, enabled: ConfigHelper.chain_type() == "optimism" +config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == "optimism" +config :indexer, Indexer.Fetcher.Optimism.Deposit.Supervisor, enabled: ConfigHelper.chain_type() == "optimism" +config :indexer, Indexer.Fetcher.Optimism.Withdrawal.Supervisor, enabled: ConfigHelper.chain_type() == "optimism" +config :indexer, Indexer.Fetcher.Optimism.WithdrawalEvent.Supervisor, enabled: ConfigHelper.chain_type() == "optimism" + +config :indexer, Indexer.Fetcher.Optimism, + optimism_l1_rpc: System.get_env("INDEXER_OPTIMISM_L1_RPC"), + optimism_l1_portal: System.get_env("INDEXER_OPTIMISM_L1_PORTAL_CONTRACT") + +config :indexer, Indexer.Fetcher.Optimism.Deposit, + start_block_l1: System.get_env("INDEXER_OPTIMISM_L1_DEPOSITS_START_BLOCK"), + batch_size: System.get_env("INDEXER_OPTIMISM_L1_DEPOSITS_BATCH_SIZE") + +config :indexer, Indexer.Fetcher.Optimism.OutputRoot, + start_block_l1: System.get_env("INDEXER_OPTIMISM_L1_OUTPUT_ROOTS_START_BLOCK"), + output_oracle: System.get_env("INDEXER_OPTIMISM_L1_OUTPUT_ORACLE_CONTRACT") + +config :indexer, Indexer.Fetcher.Optimism.Withdrawal, + start_block_l2: System.get_env("INDEXER_OPTIMISM_L2_WITHDRAWALS_START_BLOCK"), + message_passer: System.get_env("INDEXER_OPTIMISM_L2_MESSAGE_PASSER_CONTRACT") + +config :indexer, Indexer.Fetcher.Optimism.WithdrawalEvent, + start_block_l1: System.get_env("INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK") + +config :indexer, Indexer.Fetcher.Optimism.TxnBatch, + start_block_l1: System.get_env("INDEXER_OPTIMISM_L1_BATCH_START_BLOCK"), + batch_inbox: System.get_env("INDEXER_OPTIMISM_L1_BATCH_INBOX"), + batch_submitter: System.get_env("INDEXER_OPTIMISM_L1_BATCH_SUBMITTER"), + blocks_chunk_size: System.get_env("INDEXER_OPTIMISM_L1_BATCH_BLOCKS_CHUNK_SIZE", "4"), + blobs_api_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_BLOCKSCOUT_BLOBS_API_URL"), + genesis_block_l2: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_BATCH_GENESIS_BLOCK_NUMBER") config :indexer, Indexer.Fetcher.Withdrawal.Supervisor, disabled?: System.get_env("INDEXER_DISABLE_WITHDRAWALS_FETCHER", "true") == "true" config :indexer, Indexer.Fetcher.Withdrawal, first_block: System.get_env("WITHDRAWALS_FIRST_BLOCK") -config :indexer, Indexer.Fetcher.PolygonEdge.Supervisor, disabled?: !(chain_type == "polygon_edge") +config :indexer, Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, enabled: ConfigHelper.chain_type() == "polygon_edge" -config :indexer, Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, disabled?: !(chain_type == "polygon_edge") +config :indexer, Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, + enabled: ConfigHelper.chain_type() == "polygon_edge" -config :indexer, Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, disabled?: !(chain_type == "polygon_edge") +config :indexer, Indexer.Fetcher.PolygonEdge.Withdrawal.Supervisor, enabled: ConfigHelper.chain_type() == "polygon_edge" -config :indexer, Indexer.Fetcher.PolygonEdge.Withdrawal.Supervisor, disabled?: !(chain_type == "polygon_edge") - -config :indexer, Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, disabled?: !(chain_type == "polygon_edge") +config :indexer, Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, + enabled: ConfigHelper.chain_type() == "polygon_edge" config :indexer, Indexer.Fetcher.PolygonEdge, polygon_edge_l1_rpc: System.get_env("INDEXER_POLYGON_EDGE_L1_RPC"), @@ -617,14 +781,92 @@ config :indexer, Indexer.Fetcher.PolygonEdge.WithdrawalExit, start_block_l1: System.get_env("INDEXER_POLYGON_EDGE_L1_WITHDRAWALS_START_BLOCK"), exit_helper: System.get_env("INDEXER_POLYGON_EDGE_L1_EXIT_HELPER_CONTRACT") -config :indexer, Indexer.Fetcher.Zkevm.TransactionBatch, - chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_ZKEVM_BATCHES_CHUNK_SIZE", 20), - recheck_interval: ConfigHelper.parse_integer_env_var("INDEXER_ZKEVM_BATCHES_RECHECK_INTERVAL", 60) +config :indexer, Indexer.Fetcher.ZkSync.TransactionBatch, + chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_ZKSYNC_BATCHES_CHUNK_SIZE", 50), + batches_max_range: ConfigHelper.parse_integer_env_var("INDEXER_ZKSYNC_NEW_BATCHES_MAX_RANGE", 50), + recheck_interval: ConfigHelper.parse_integer_env_var("INDEXER_ZKSYNC_NEW_BATCHES_RECHECK_INTERVAL", 60) + +config :indexer, Indexer.Fetcher.ZkSync.TransactionBatch.Supervisor, + enabled: ConfigHelper.parse_bool_env_var("INDEXER_ZKSYNC_BATCHES_ENABLED") + +config :indexer, Indexer.Fetcher.ZkSync.BatchesStatusTracker, + zksync_l1_rpc: System.get_env("INDEXER_ZKSYNC_L1_RPC"), + recheck_interval: ConfigHelper.parse_integer_env_var("INDEXER_ZKSYNC_BATCHES_STATUS_RECHECK_INTERVAL", 60) -config :indexer, Indexer.Fetcher.Zkevm.TransactionBatch.Supervisor, +config :indexer, Indexer.Fetcher.ZkSync.BatchesStatusTracker.Supervisor, + enabled: ConfigHelper.parse_bool_env_var("INDEXER_ZKSYNC_BATCHES_ENABLED") + +config :indexer, Indexer.Fetcher.RootstockData.Supervisor, + disabled?: + ConfigHelper.chain_type() != "rsk" || ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_ROOTSTOCK_DATA_FETCHER") + +config :indexer, Indexer.Fetcher.RootstockData, + interval: ConfigHelper.parse_time_env_var("INDEXER_ROOTSTOCK_DATA_FETCHER_INTERVAL", "3s"), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_ROOTSTOCK_DATA_FETCHER_BATCH_SIZE", 10), + max_concurrency: ConfigHelper.parse_integer_env_var("INDEXER_ROOTSTOCK_DATA_FETCHER_CONCURRENCY", 5), + db_batch_size: ConfigHelper.parse_integer_env_var("INDEXER_ROOTSTOCK_DATA_FETCHER_DB_BATCH_SIZE", 300) + +config :indexer, Indexer.Fetcher.Beacon, beacon_rpc: System.get_env("INDEXER_BEACON_RPC_URL") || "http://localhost:5052" + +config :indexer, Indexer.Fetcher.Beacon.Blob.Supervisor, + disabled?: + ConfigHelper.chain_type() != "ethereum" || + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_BEACON_BLOB_FETCHER") + +config :indexer, Indexer.Fetcher.Beacon.Blob, + slot_duration: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_SLOT_DURATION", 12), + reference_slot: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_REFERENCE_SLOT", 8_000_000), + reference_timestamp: + ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_REFERENCE_TIMESTAMP", 1_702_824_023), + start_block: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_START_BLOCK", 19_200_000), + end_block: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_END_BLOCK", 0) + +config :indexer, Indexer.Fetcher.Shibarium.L1, + rpc: System.get_env("INDEXER_SHIBARIUM_L1_RPC"), + start_block: System.get_env("INDEXER_SHIBARIUM_L1_START_BLOCK"), + deposit_manager_proxy: System.get_env("INDEXER_SHIBARIUM_L1_DEPOSIT_MANAGER_CONTRACT"), + ether_predicate_proxy: System.get_env("INDEXER_SHIBARIUM_L1_ETHER_PREDICATE_CONTRACT"), + erc20_predicate_proxy: System.get_env("INDEXER_SHIBARIUM_L1_ERC20_PREDICATE_CONTRACT"), + erc721_predicate_proxy: System.get_env("INDEXER_SHIBARIUM_L1_ERC721_PREDICATE_CONTRACT"), + erc1155_predicate_proxy: System.get_env("INDEXER_SHIBARIUM_L1_ERC1155_PREDICATE_CONTRACT"), + withdraw_manager_proxy: System.get_env("INDEXER_SHIBARIUM_L1_WITHDRAW_MANAGER_CONTRACT") + +config :indexer, Indexer.Fetcher.Shibarium.L2, + start_block: System.get_env("INDEXER_SHIBARIUM_L2_START_BLOCK"), + child_chain: System.get_env("INDEXER_SHIBARIUM_L2_CHILD_CHAIN_CONTRACT"), + weth: System.get_env("INDEXER_SHIBARIUM_L2_WETH_CONTRACT"), + bone_withdraw: System.get_env("INDEXER_SHIBARIUM_L2_BONE_WITHDRAW_CONTRACT") + +config :indexer, Indexer.Fetcher.Shibarium.L1.Supervisor, enabled: ConfigHelper.chain_type() == "shibarium" + +config :indexer, Indexer.Fetcher.Shibarium.L2.Supervisor, enabled: ConfigHelper.chain_type() == "shibarium" + +config :indexer, Indexer.Fetcher.PolygonZkevm.BridgeL1, + rpc: System.get_env("INDEXER_POLYGON_ZKEVM_L1_RPC"), + start_block: System.get_env("INDEXER_POLYGON_ZKEVM_L1_BRIDGE_START_BLOCK"), + bridge_contract: System.get_env("INDEXER_POLYGON_ZKEVM_L1_BRIDGE_CONTRACT"), + native_symbol: System.get_env("INDEXER_POLYGON_ZKEVM_L1_BRIDGE_NATIVE_SYMBOL", "ETH"), + native_decimals: ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_ZKEVM_L1_BRIDGE_NATIVE_DECIMALS", 18) + +config :indexer, Indexer.Fetcher.PolygonZkevm.BridgeL1.Supervisor, enabled: ConfigHelper.chain_type() == "polygon_zkevm" + +config :indexer, Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens.Supervisor, + enabled: ConfigHelper.chain_type() == "polygon_zkevm" + +config :indexer, Indexer.Fetcher.PolygonZkevm.BridgeL2, + start_block: System.get_env("INDEXER_POLYGON_ZKEVM_L2_BRIDGE_START_BLOCK"), + bridge_contract: System.get_env("INDEXER_POLYGON_ZKEVM_L2_BRIDGE_CONTRACT") + +config :indexer, Indexer.Fetcher.PolygonZkevm.BridgeL2.Supervisor, enabled: ConfigHelper.chain_type() == "polygon_zkevm" + +config :indexer, Indexer.Fetcher.PolygonZkevm.TransactionBatch, + chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_ZKEVM_BATCHES_CHUNK_SIZE", 20), + recheck_interval: ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_ZKEVM_BATCHES_RECHECK_INTERVAL", 60) + +config :indexer, Indexer.Fetcher.PolygonZkevm.TransactionBatch.Supervisor, enabled: - System.get_env("CHAIN_TYPE", "ethereum") == "polygon_zkevm" && - ConfigHelper.parse_bool_env_var("INDEXER_ZKEVM_BATCHES_ENABLED") + ConfigHelper.chain_type() == "polygon_zkevm" && + ConfigHelper.parse_bool_env_var("INDEXER_POLYGON_ZKEVM_BATCHES_ENABLED") Code.require_file("#{config_env()}.exs", "config/runtime") diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index c10a0b752a5c..a8f21fdbbbbd 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -42,12 +42,15 @@ pool_size = do: ConfigHelper.parse_integer_env_var("POOL_SIZE", 30), else: ConfigHelper.parse_integer_env_var("POOL_SIZE", 40) +queue_target = ConfigHelper.parse_integer_env_var("DATABASE_QUEUE_TARGET", 50) + # Configure your database config :explorer, Explorer.Repo, database: database, hostname: hostname, url: System.get_env("DATABASE_URL"), - pool_size: pool_size + pool_size: pool_size, + queue_target: queue_target database_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: database hostname_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: hostname @@ -57,7 +60,8 @@ config :explorer, Explorer.Repo.Replica1, database: database_api, hostname: hostname_api, url: ExplorerConfigHelper.get_api_db_url(), - pool_size: ConfigHelper.parse_integer_env_var("POOL_SIZE_API", 10) + pool_size: ConfigHelper.parse_integer_env_var("POOL_SIZE_API", 10), + queue_target: queue_target database_account = if System.get_env("ACCOUNT_DATABASE_URL"), do: nil, else: database hostname_account = if System.get_env("ACCOUNT_DATABASE_URL"), do: nil, else: hostname @@ -67,7 +71,33 @@ config :explorer, Explorer.Repo.Account, database: database_account, hostname: hostname_account, url: ExplorerConfigHelper.get_account_db_url(), - pool_size: ConfigHelper.parse_integer_env_var("ACCOUNT_POOL_SIZE", 10) + pool_size: ConfigHelper.parse_integer_env_var("ACCOUNT_POOL_SIZE", 10), + queue_target: queue_target + +# Configure Beacon Chain database +config :explorer, Explorer.Repo.Beacon, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1 + +# Configures BridgedTokens database +config :explorer, Explorer.Repo.BridgedTokens, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1 + +# Configure Optimism database +config :explorer, Explorer.Repo.Optimism, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + pool_size: 1 # Configure PolygonEdge database config :explorer, Explorer.Repo.PolygonEdge, @@ -87,6 +117,15 @@ config :explorer, Explorer.Repo.PolygonZkevm, # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type pool_size: 1 +# Configure ZkSync database +config :explorer, Explorer.Repo.ZkSync, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1 + # Configure Rootstock database config :explorer, Explorer.Repo.RSK, database: database, @@ -96,6 +135,13 @@ config :explorer, Explorer.Repo.RSK, # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type pool_size: 1 +# Configure Shibarium database +config :explorer, Explorer.Repo.Shibarium, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + pool_size: 1 + # Configure Suave database config :explorer, Explorer.Repo.Suave, database: database, @@ -103,6 +149,20 @@ config :explorer, Explorer.Repo.Suave, url: ExplorerConfigHelper.get_suave_db_url(), pool_size: 1 +# Configure Filecoin database +config :explorer, Explorer.Repo.Filecoin, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + pool_size: 1 + +# Configures Stability database +config :explorer, Explorer.Repo.Stability, + database: database, + hostname: hostname, + url: System.get_env("DATABASE_URL"), + pool_size: 1 + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/dev") diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index a8692d8c540e..cabdab7b9429 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -28,23 +28,49 @@ config :block_scout_web, BlockScoutWeb.Endpoint, ################ pool_size = ConfigHelper.parse_integer_env_var("POOL_SIZE", 50) +queue_target = ConfigHelper.parse_integer_env_var("DATABASE_QUEUE_TARGET", 50) # Configures the database config :explorer, Explorer.Repo, url: System.get_env("DATABASE_URL"), pool_size: pool_size, - ssl: ExplorerConfigHelper.ssl_enabled?() + ssl: ExplorerConfigHelper.ssl_enabled?(), + queue_target: queue_target # Configures API the database config :explorer, Explorer.Repo.Replica1, url: ExplorerConfigHelper.get_api_db_url(), pool_size: ConfigHelper.parse_integer_env_var("POOL_SIZE_API", 50), - ssl: ExplorerConfigHelper.ssl_enabled?() + ssl: ExplorerConfigHelper.ssl_enabled?(), + queue_target: queue_target # Configures Account database config :explorer, Explorer.Repo.Account, url: ExplorerConfigHelper.get_account_db_url(), pool_size: ConfigHelper.parse_integer_env_var("ACCOUNT_POOL_SIZE", 50), + ssl: ExplorerConfigHelper.ssl_enabled?(), + queue_target: queue_target + +# Configure Beacon Chain database +config :explorer, Explorer.Repo.Beacon, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + +# Configures BridgedTokens database +config :explorer, Explorer.Repo.BridgedTokens, + url: System.get_env("DATABASE_URL"), + # actually this repo is not started, and its pool size remains unused. + # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + +# Configures Optimism database +config :explorer, Explorer.Repo.Optimism, + url: System.get_env("DATABASE_URL"), + pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() # Configures PolygonEdge database @@ -57,6 +83,12 @@ config :explorer, Explorer.Repo.PolygonEdge, # Configures PolygonZkevm database config :explorer, Explorer.Repo.PolygonZkevm, + url: System.get_env("DATABASE_URL"), + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + +# Configures ZkSync database +config :explorer, Explorer.Repo.ZkSync, url: System.get_env("DATABASE_URL"), # actually this repo is not started, and its pool size remains unused. # separating repos for different CHAIN_TYPE is implemented only for the sake of keeping DB schema update relevant to the current chain type @@ -71,12 +103,30 @@ config :explorer, Explorer.Repo.RSK, pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() +# Configures Shibarium database +config :explorer, Explorer.Repo.Shibarium, + url: System.get_env("DATABASE_URL"), + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + # Configures Suave database config :explorer, Explorer.Repo.Suave, url: ExplorerConfigHelper.get_suave_db_url(), pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() +# Configures Filecoin database +config :explorer, Explorer.Repo.Filecoin, + url: System.get_env("DATABASE_URL"), + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + +# Configures Stability database +config :explorer, Explorer.Repo.Stability, + url: System.get_env("DATABASE_URL"), + pool_size: 1, + ssl: ExplorerConfigHelper.ssl_enabled?() + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/prod") diff --git a/config/runtime/test.exs b/config/runtime/test.exs index ca3eed98e10c..786755f81298 100644 --- a/config/runtime/test.exs +++ b/config/runtime/test.exs @@ -16,6 +16,10 @@ config :block_scout_web, BlockScoutWeb.API.V2, enabled: true ### Explorer ### ################ +config :explorer, Explorer.Counters.Transactions24hStats, + cache_period: ConfigHelper.parse_time_env_var("CACHE_TRANSACTIONS_24H_STATS_PERIOD", "1h"), + enable_consolidation: false + variant = Variant.get() Code.require_file("#{variant}.exs", "apps/explorer/config/test") diff --git a/config/test.exs b/config/test.exs index 92ddca6a93a0..c979cd834877 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,6 +4,8 @@ import Config config :logger, :console, level: :warn +config :logger_json, :backend, level: :none + config :logger, :ecto, level: :warn, path: Path.absname("logs/test/ecto.log") diff --git a/cspell.json b/cspell.json index 2ef306d4e72d..ec008f2588e1 100644 --- a/cspell.json +++ b/cspell.json @@ -9,28 +9,113 @@ "apps/block_scout_web/assets/js/lib/ace/src-min/*.js" ], "words": [ + "AION", + "AIRTABLE", + "ARGMAX", + "Aiubo", + "Arbitrum", + "Asfpp", + "Asfpp", + "Autodetection", + "Autonity", + "Blockchair", + "CALLCODE", + "CBOR", + "Cldr", + "Consolas", + "Cyclomatic", + "DATETIME", + "DELEGATECALL", + "Decompiler", + "DefiLlama", + "DefiLlama", + "Denormalization", + "Denormalized", + "ECTO", + "EDCSA", + "Ebhwp", + "Encryptor", + "Erigon", + "Ethash", + "Faileddi", + "Filesize", + "Floki", + "Fuov", + "Hazkne", + "Hodl", + "Iframe", + "Iframes", + "Incrementer", + "Instrumenter", + "Karnaugh", + "Keepalive", + "LUKSO", + "Limegreen", + "MARKETCAP", + "MDWW", + "Mainnets", + "Mendonça", + "Menlo", + "Merkle", + "Mixfile", + "NOTOK", + "Nerg", + "Nerg", + "Nethermind", + "Neue", + "Njhr", + "Nodealus", + "NovesFi", + "Numbe", + "Nunito", + "PGDATABASE", + "PGHOST", + "PGPASSWORD", + "PGPORT", + "PGUSER", + "POSDAO", + "Posix", + "Postrge", + "Qebz", + "Qmbgk", + "REINDEX", + "RPC's", + "RPCs", + "SENDGRID", + "SJONRPC", + "SOLIDITYSCAN", + "SOLIDITYSCAN", + "STATICCALL", + "Secon", + "Segoe", + "Sokol", + "Synthereum", + "Sérgio", + "Tcnwg", + "Testinit", + "Testit", + "Testname", + "Txns", + "UUPS", + "Unitarion", + "Unitorius", + "Unitorus", + "Utqn", + "Wanchain", "aave", "absname", "acbs", "accs", "actb", "addedfile", - "AION", - "AIRTABLE", - "Aiubo", "alloc", "amzootyukbugmx", "apikey", - "Arbitrum", - "ARGMAX", "arounds", "asda", - "Asfpp", "atoken", "autodetectfalse", - "Autodetection", "autodetecttrue", - "Autonity", "autoplay", "backoff", "badhash", @@ -46,13 +131,14 @@ "bigserial", "binwrite", "bizbuz", - "Blockchair", "blockheight", "blockless", "blockno", + "blockreward", "blockscout", "blockscoutuser", "bools", + "bridgedtokenlist", "browserconfig", "bsdr", "buildcache", @@ -60,26 +146,26 @@ "buildx", "bytea", "bytecodes", + "byteshash", "byts", "bzzr", "cacerts", "callcode", - "CALLCODE", "calltracer", "capturelog", "cattributes", - "CBOR", "cellspacing", "certifi", "cfasync", + "chainid", "chainlink", "chakra", "chartjs", + "checkproxyverification", "checksummed", "checkverifystatus", "childspec", "citext", - "Cldr", "clearfix", "clickover", "codeformat", @@ -97,36 +183,33 @@ "compilerversion", "concache", "cond", - "Consolas", "contractaddress", + "contractaddresses", "contractname", "cooldown", "cooltesthost", "crossorigin", + "CRYPTOCOMPARE", "ctbs", "ctid", "cumalative", - "Cyclomatic", "cypherpunk", "czilladx", "datapoint", "datepicker", - "DATETIME", "deae", "decamelize", "decompiled", "decompiler", - "Decompiler", "dedup", "defmock", "defsupervisor", "dejob", "dejobio", "delegatecall", - "DELEGATECALL", "delegators", "demonitor", - "Denormalized", + "denormalization", "descr", "describedby", "differenceby", @@ -134,50 +217,53 @@ "dropzone", "dxgd", "dyntsrohg", - "Ebhwp", "econnrefused", - "ECTO", - "EDCSA", "edhygl", "efkuga", - "Encryptor", "endregion", "enetunreach", "enoent", "epns", - "Erigon", "errora", "errorb", "erts", - "Ethash", + "erts", "etherchain", + "ethprice", "ethsupply", "ethsupplyexchange", "etimedout", "eveem", "evenodd", + "evmversion", + "exitor", "explorable", "exponention", "extcodehash", "extname", "extremums", "exvcr", - "Faileddi", "falala", + "FEVM", + "filecoin", + "Filecoin", "Filesize", - "Floki", + "Filecoin", + "fkey", + "fkey", "fontawesome", "fortawesome", "fsym", "fullwidth", - "Fuov", "fvdskvjglav", "fwrite", "fwupv", "getabi", "getblockbyhash", + "getblockcountdown", "getblocknobytime", "getblockreward", + "getcontractcreation", "getlogs", "getminedblocks", "getsourcecode", @@ -193,24 +279,22 @@ "graphiql", "grecaptcha", "greymatter", + "gtag", "happygokitty", "haspopup", - "Hazkne", "histoday", "hljs", "Hodl", + "HOPR", "httpoison", + "hyperledger", "ifdef", "ifeq", - "Iframe", "iframes", - "Iframes", "ilike", "illustr", "inapp", - "Incrementer", "insertable", - "Instrumenter", "intersectionby", "ints", "invalidend", @@ -221,22 +305,24 @@ "invalidstart", "inversed", "ipfs", + "ipos", "itxs", "johnnny", "jsons", "juon", - "Karnaugh", "keccak", - "Keepalive", "keyout", "kittencream", "labeledby", "labelledby", + "lastmod", + "lastmod", "lastname", "lastword", "lformat", + "libraryaddress", + "libraryname", "libsecp", - "Limegreen", "linecap", "linejoin", "listaccounts", @@ -244,28 +330,23 @@ "lkve", "llhauc", "loggable", - "LUKSO", "luxon", "mabi", - "Mainnets", "malihu", "mallowance", - "MARKETCAP", "maxlength", "mcap", "mconst", "mdef", - "MDWW", - "Mendonça", - "Menlo", + "meer", + "meer", "mergeable", - "Merkle", "metatags", + "microsecs", "millis", "mintings", "mistmatches", "miterlimit", - "Mixfile", "mmem", "mname", "mnot", @@ -287,17 +368,12 @@ "mydep", "nanomorph", "nbsp", - "Nerg", - "Nethermind", - "Neue", "newkey", "nftproduct", "ngettext", "nillifies", - "Njhr", "nlmyzui", "nocheck", - "Nodealus", "nohighlight", "nolink", "nonconsensus", @@ -305,17 +381,19 @@ "noproc", "noreferrer", "noreply", + "NOTOK", + "noves", "nowarn", "nowrap", "ntoa", - "Numbe", - "Nunito", "nxdomain", "omni", "onclick", "onconnect", "ondisconnect", + "opos", "outcoming", + "overengineering", "pawesome", "pbcopy", "peeker", @@ -323,11 +401,6 @@ "pendingtxlist", "perc", "persistable", - "PGDATABASE", - "PGHOST", - "PGPASSWORD", - "PGPORT", - "PGUSER", "phash", "pikaday", "pkey", @@ -340,23 +413,22 @@ "pocc", "polyline", "poolboy", - "POSDAO", - "Posix", - "Postrge", "prederive", "prederived", "progressbar", + "proxiable", "psql", "purrstige", "qdai", - "Qebz", - "Qmbgk", + "qitmeer", + "qitmeer", "qrcode", "queriable", "questiona", "questionb", "qwertyufhgkhiop", "qwertyuioiuytrewertyuioiuytrertyuio", + "qwertyuioiuytrewertyuioiuytrertyuio", "racecar", "raisedbrow", "rangeright", @@ -369,8 +441,10 @@ "recollated", "redix", "refetched", + "regclass", "REINDEX", "relname", + "relpages", "reltuples", "remasc", "removedfile", @@ -379,29 +453,31 @@ "rerequest", "reshows", "retryable", + "returnaddress", "reuseaddr", + "rollup", + "rollups", "RPC's", "RPCs", "safelow", "savechives", - "Secon", "secp", - "Segoe", + "secp", "seindexed", "selfdestruct", "selfdestructed", "SENDGRID", + "Sepolia", "Sérgio", "sharelock", "sharelocks", + "shibarium", "shortdoc", "shortify", - "SJONRPC", "smallint", "smth", "snapshotted", "snapshotting", - "Sokol", "soljson", "someout", "sourcecode", @@ -412,8 +488,8 @@ "stakers", "stateroot", "staticcall", - "STATICCALL", "strftime", + "strhash", "stringly", "stylelint", "stylesheet", @@ -429,28 +505,26 @@ "successa", "successb", "supernet", + "sushiswap", "swal", "sweetalert", - "Synthereum", "tabindex", "tablist", "tabpanel", "tarekraafat", "tbody", "tbrf", - "Tcnwg", "tems", - "Testinit", - "Testit", - "Testname", "testpassword", "testtest", "testuser", "thead", "thicccbrowz", "throttleable", + "timestmaps", "tokenbalance", "tokenlist", + "tokennfttx", "tokensupply", "tokentx", "topbar", @@ -463,8 +537,8 @@ "tsquery", "tsvector", "tsym", + "txid", "txlistinternal", - "Txns", "txpool", "txreceipt", "ueberauth", @@ -473,9 +547,6 @@ "unclosable", "unfetched", "unfinalized", - "Unitarion", - "Unitorius", - "Unitorus", "unknownc", "unknowne", "unmarshal", @@ -489,18 +560,20 @@ "upserting", "upserts", "urijs", - "Utqn", + "urlset", + "urlset", "valign", "valuemax", "valuemin", "valuenow", + "varint", + "verifyproxycontract", "verifysourcecode", "viewerjs", "volumefrom", "volumeto", "vyper", "walletconnect", - "Wanchain", "warninga", "warningb", "watchlist", @@ -521,10 +594,12 @@ "yellowgreen", "zaphod", "zeppelinos", + "zetachain", "zftv", "ziczr", "zindex", "zipcode", + "zkatana", "zkbob", "zkevm", "erts", @@ -536,7 +611,23 @@ "lastmod", "qitmeer", "meer", - "DefiLlama" + "DefiLlama", + "SOLIDITYSCAN", + "fkey", + "getcontractcreation", + "contractaddresses", + "tokennfttx", + "libraryname", + "libraryaddress", + "evmversion", + "verifyproxycontract", + "checkproxyverification", + "NOTOK", + "sushiswap", + "zetachain", + "zksync", + "filecoin", + "Filecoin" ], "enableFiletypes": [ "dotenv", diff --git a/docker-compose/README.md b/docker-compose/README.md index ded1ce9ad0b4..cc472ebd4c40 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -37,15 +37,21 @@ and 4 containers for microservices (written in Rust): The repo contains built-in configs for different JSON RPC clients without need to build the image. -- Erigon: `docker-compose -f docker-compose-no-build-erigon.yml up -d` -- Geth (suitable for Reth as well): `docker-compose -f docker-compose-no-build-geth.yml up -d` -- Geth Clique: `docker-compose -f docker-compose-no-build-geth-clique-consensus.yml up -d` -- Nethermind, OpenEthereum: `docker-compose -f docker-compose-no-build-nethermind up -d` -- Ganache: `docker-compose -f docker-compose-no-build-ganache.yml up -d` -- HardHat network: `docker-compose -f docker-compose-no-build-hardhat-network.yml up -d` -- Running only explorer without DB: `docker-compose -f docker-compose-no-build-no-db-container.yml up -d`. In this case, one container is created - for the explorer itself. And it assumes that the DB credentials are provided through `DATABASE_URL` environment variable. -- Running explorer with external backend: `docker-compose -f docker-compose-no-build-external-backend.yml up -d` -- Running explorer with external frontend: `docker-compose -f docker-compose-no-build-external-frontend.yml up -d` +**Note**: in all below examples, you can use `docker compose` instead of `docker-compose`, if compose v2 plugin is installed in Docker. + +| __JSON RPC Client__ | __Docker compose launch command__ | +| -------- | ------- | +| Erigon | `docker-compose -f erigon.yml up -d` | +| Geth (suitable for Reth as well) | `docker-compose -f geth.yml up -d` | +| Geth Clique | `docker-compose -f geth-clique-consensus.yml up -d` | +| Nethermind, OpenEthereum | `docker-compose -f nethermind up -d` | +| Ganache | `docker-compose -f ganache.yml up -d` | +| HardHat network | `docker-compose -f hardhat-network.yml up -d` | + +- Running only explorer without DB: `docker-compose -f external-db.yml up -d`. In this case, no db container is created. And it assumes that the DB credentials are provided through `DATABASE_URL` environment variable on the backend container. +- Running explorer with external backend: `docker-compose -f external-backend.yml up -d` +- Running explorer with external frontend: `docker-compose -f external-frontend.yml up -d` +- Running all microservices: `docker-compose -f microservices.yml up -d` All of the configs assume the Ethereum JSON RPC is running at http://localhost:8545. diff --git a/docker-compose/docker-compose-no-build-nethermind.yml b/docker-compose/docker-compose-no-build-nethermind.yml deleted file mode 100644 index 77c5e5eef9b7..000000000000 --- a/docker-compose/docker-compose-no-build-nethermind.yml +++ /dev/null @@ -1,74 +0,0 @@ -version: '3.9' - -services: - redis_db: - extends: - file: ./services/docker-compose-redis.yml - service: redis_db - - db-init: - extends: - file: ./services/docker-compose-db.yml - service: db-init - - db: - extends: - file: ./services/docker-compose-db.yml - service: db - - backend: - depends_on: - - db - - redis_db - extends: - file: ./services/docker-compose-backend.yml - service: backend - links: - - db:database - environment: - ETHEREUM_JSONRPC_VARIANT: 'nethermind' - - visualizer: - extends: - file: ./services/docker-compose-visualizer.yml - service: visualizer - - sig-provider: - extends: - file: ./services/docker-compose-sig-provider.yml - service: sig-provider - - frontend: - depends_on: - - backend - extends: - file: ./services/docker-compose-frontend.yml - service: frontend - - stats-db-init: - extends: - file: ./services/docker-compose-stats.yml - service: stats-db-init - - stats-db: - depends_on: - - backend - extends: - file: ./services/docker-compose-stats.yml - service: stats-db - - stats: - depends_on: - - stats-db - extends: - file: ./services/docker-compose-stats.yml - service: stats - - proxy: - depends_on: - - backend - - frontend - - stats - extends: - file: ./services/docker-compose-nginx.yml - service: proxy diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index c5f140a31263..f306bb7036fa 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend build: context: .. @@ -34,7 +37,7 @@ services: CACHE_TOTAL_GAS_USAGE_COUNTER_ENABLED: "" CACHE_ADDRESS_WITH_BALANCES_UPDATE_INTERVAL: "" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 5.3.1 + RELEASE_VERSION: 6.3.0 links: - db:database environment: @@ -45,38 +48,40 @@ services: visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -85,5 +90,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 3b5ae9a7d212..11d5e7872571 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -1,10 +1,12 @@ -# DOCKER_TAG= ETHEREUM_JSONRPC_VARIANT=geth ETHEREUM_JSONRPC_HTTP_URL=http://host.docker.internal:8545/ # ETHEREUM_JSONRPC_FALLBACK_HTTP_URL= DATABASE_URL=postgresql://blockscout:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout +# DATABASE_QUEUE_TARGET ETHEREUM_JSONRPC_TRACE_URL=http://host.docker.internal:8545/ # ETHEREUM_JSONRPC_FALLBACK_TRACE_URL= +# ETHEREUM_JSONRPC_FALLBACK_ETH_CALL_URL= +# ETHEREUM_JSONRPC_ETH_CALL_URL= # ETHEREUM_JSONRPC_HTTP_TIMEOUT= # CHAIN_TYPE= NETWORK= @@ -13,9 +15,10 @@ LOGO=/images/blockscout_logo.svg # ETHEREUM_JSONRPC_WS_URL= ETHEREUM_JSONRPC_TRANSPORT=http ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES=false -#ETHEREUM_JSONRPC_ARCHIVE_BALANCES_WINDOW=200 +# ETHEREUM_JSONRPC_ARCHIVE_BALANCES_WINDOW=200 # ETHEREUM_JSONRPC_HTTP_HEADERS= # ETHEREUM_JSONRPC_WAIT_PER_TIMEOUT= +# ETHEREUM_JSONRPC_GETH_TRACE_BY_BLOCK= IPC_PATH= NETWORK_PATH=/ BLOCKSCOUT_HOST= @@ -38,9 +41,12 @@ EXCHANGE_RATES_COIN= # EXCHANGE_RATES_TVL_SOURCE= # EXCHANGE_RATES_PRICE_SOURCE= # EXCHANGE_RATES_COINGECKO_COIN_ID= +# EXCHANGE_RATES_COINGECKO_SECONDARY_COIN_ID= # EXCHANGE_RATES_COINGECKO_API_KEY= # EXCHANGE_RATES_COINMARKETCAP_API_KEY= # EXCHANGE_RATES_COINMARKETCAP_COIN_ID= +# EXCHANGE_RATES_COINMARKETCAP_SECONDARY_COIN_ID= +# EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL= POOL_SIZE=80 # EXCHANGE_RATES_COINGECKO_PLATFORM_ID= # TOKEN_EXCHANGE_RATE_INTERVAL= @@ -55,13 +61,14 @@ ECTO_USE_SSL=false # SPANDEX_SYNC_THRESHOLD= HEART_BEAT_TIMEOUT=30 # HEART_COMMAND= -#BLOCKSCOUT_VERSION= +# BLOCKSCOUT_VERSION= RELEASE_LINK= BLOCK_TRANSFORMER=base # GRAPHIQL_TRANSACTION= # BLOCK_RANGES= # FIRST_BLOCK= # LAST_BLOCK= +# TRACE_BLOCK_RANGES= # TRACE_FIRST_BLOCK= # TRACE_LAST_BLOCK= # FOOTER_CHAT_LINK= @@ -87,24 +94,46 @@ CACHE_MARKET_HISTORY_PERIOD=21600 CACHE_ADDRESS_TRANSACTIONS_COUNTER_PERIOD=1800 CACHE_ADDRESS_TOKENS_USD_SUM_PERIOD=3600 CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD=1800 +# CACHE_TRANSACTIONS_24H_STATS_PERIOD= +# CACHE_FRESH_PENDING_TRANSACTIONS_COUNTER_PERIOD= TOKEN_METADATA_UPDATE_INTERVAL=172800 -CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,default -CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,default +CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,default +CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default # CONTRACT_VERIFICATION_MAX_LIBRARIES=10 CONTRACT_MAX_STRING_LENGTH_WITHOUT_TRIMMING=2040 # CONTRACT_DISABLE_INTERACTION= +# CONTRACT_AUDIT_REPORTS_AIRTABLE_URL= +# CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY= UNCLES_IN_AVERAGE_BLOCK_TIME=false DISABLE_WEBAPP=false +API_V2_ENABLED=true API_V1_READ_METHODS_DISABLED=false API_V1_WRITE_METHODS_DISABLED=false +# API_RATE_LIMIT_DISABLED=true +# API_SENSITIVE_ENDPOINTS_KEY= +API_RATE_LIMIT_TIME_INTERVAL=1s +API_RATE_LIMIT_BY_IP_TIME_INTERVAL=5m +API_RATE_LIMIT=50 +API_RATE_LIMIT_BY_KEY=50 +API_RATE_LIMIT_BY_WHITELISTED_IP=50 +API_RATE_LIMIT_WHITELISTED_IPS= +API_RATE_LIMIT_STATIC_API_KEY= +API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 +API_RATE_LIMIT_BY_IP=3000 DISABLE_INDEXER=false DISABLE_REALTIME_INDEXER=false +DISABLE_CATCHUP_INDEXER=false +INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER=false INDEXER_DISABLE_TOKEN_INSTANCE_REALTIME_FETCHER=false INDEXER_DISABLE_TOKEN_INSTANCE_RETRY_FETCHER=false INDEXER_DISABLE_TOKEN_INSTANCE_SANITIZE_FETCHER=false INDEXER_DISABLE_TOKEN_INSTANCE_LEGACY_SANITIZE_FETCHER=false INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER=false INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false +# INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER= +# INDEXER_DISABLE_BLOCK_REWARD_FETCHER= +# INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER= +# INDEXER_DISABLE_WITHDRAWALS_FETCHER= # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE= # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_CATCHUP_BLOCK_INTERVAL= @@ -113,11 +142,25 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY= # INDEXER_BLOCK_REWARD_BATCH_SIZE= # INDEXER_BLOCK_REWARD_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_USE_BASE_URI_RETRY= # INDEXER_TOKEN_INSTANCE_RETRY_REFETCH_INTERVAL= +# INDEXER_TOKEN_INSTANCE_RETRY_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_RETRY_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_REALTIME_BATCH_SIZE=1 # INDEXER_TOKEN_INSTANCE_REALTIME_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_SANITIZE_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_SANITIZE_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_CONCURRENCY=10 +# INDEXER_DISABLE_TOKEN_INSTANCE_ERC_1155_SANITIZE_FETCHER=false +# INDEXER_DISABLE_TOKEN_INSTANCE_ERC_721_SANITIZE_FETCHER=false +# INDEXER_TOKEN_INSTANCE_ERC_1155_SANITIZE_CONCURRENCY=2 +# INDEXER_TOKEN_INSTANCE_ERC_1155_SANITIZE_BATCH_SIZE=10 +# INDEXER_TOKEN_INSTANCE_ERC_721_SANITIZE_CONCURRENCY=2 +# INDEXER_TOKEN_INSTANCE_ERC_721_SANITIZE_BATCH_SIZE=10 +# INDEXER_TOKEN_INSTANCE_ERC_721_SANITIZE_TOKENS_BATCH_SIZE=100 +# TOKEN_INSTANCE_OWNER_MIGRATION_CONCURRENCY=5 +# TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE=50 # INDEXER_COIN_BALANCES_BATCH_SIZE= # INDEXER_COIN_BALANCES_CONCURRENCY= # INDEXER_RECEIPTS_BATCH_SIZE= @@ -141,19 +184,57 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # INDEXER_POLYGON_EDGE_L2_STATE_RECEIVER_CONTRACT= # INDEXER_POLYGON_EDGE_L2_DEPOSITS_START_BLOCK= # INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE= -# INDEXER_ZKEVM_BATCHES_ENABLED= -# INDEXER_ZKEVM_BATCHES_CHUNK_SIZE= -# INDEXER_ZKEVM_BATCHES_RECHECK_INTERVAL= +# INDEXER_POLYGON_ZKEVM_BATCHES_ENABLED= +# INDEXER_POLYGON_ZKEVM_BATCHES_CHUNK_SIZE= +# INDEXER_POLYGON_ZKEVM_BATCHES_RECHECK_INTERVAL= +# INDEXER_POLYGON_ZKEVM_L1_RPC= +# INDEXER_POLYGON_ZKEVM_L1_BRIDGE_START_BLOCK= +# INDEXER_POLYGON_ZKEVM_L1_BRIDGE_CONTRACT= +# INDEXER_POLYGON_ZKEVM_L1_BRIDGE_NATIVE_SYMBOL= +# INDEXER_POLYGON_ZKEVM_L1_BRIDGE_NATIVE_DECIMALS= +# INDEXER_POLYGON_ZKEVM_L2_BRIDGE_START_BLOCK= +# INDEXER_POLYGON_ZKEVM_L2_BRIDGE_CONTRACT= +# INDEXER_ZKSYNC_BATCHES_ENABLED= +# INDEXER_ZKSYNC_BATCHES_CHUNK_SIZE= +# INDEXER_ZKSYNC_NEW_BATCHES_MAX_RANGE= +# INDEXER_ZKSYNC_NEW_BATCHES_RECHECK_INTERVAL= +# INDEXER_ZKSYNC_L1_RPC= +# INDEXER_ZKSYNC_BATCHES_STATUS_RECHECK_INTERVAL= # INDEXER_REALTIME_FETCHER_MAX_GAP= # INDEXER_FETCHER_INIT_QUERY_LIMIT= # INDEXER_TOKEN_BALANCES_FETCHER_INIT_QUERY_LIMIT= # INDEXER_COIN_BALANCES_FETCHER_INIT_QUERY_LIMIT= -# INDEXER_DISABLE_WITHDRAWALS_FETCHER= # WITHDRAWALS_FIRST_BLOCK= +# INDEXER_OPTIMISM_L1_RPC= +# INDEXER_OPTIMISM_L1_BATCH_START_BLOCK= +# INDEXER_OPTIMISM_L1_BATCH_INBOX= +# INDEXER_OPTIMISM_L1_BATCH_SUBMITTER= +# INDEXER_OPTIMISM_L1_BATCH_BLOCKS_CHUNK_SIZE= +# INDEXER_OPTIMISM_L2_BATCH_GENESIS_BLOCK_NUMBER= +# INDEXER_OPTIMISM_L1_PORTAL_CONTRACT= +# INDEXER_OPTIMISM_L1_OUTPUT_ROOTS_START_BLOCK= +# INDEXER_OPTIMISM_L1_OUTPUT_ORACLE_CONTRACT= +# INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK= +# INDEXER_OPTIMISM_L2_WITHDRAWALS_START_BLOCK= +# INDEXER_OPTIMISM_L2_MESSAGE_PASSER_CONTRACT= +# INDEXER_OPTIMISM_L1_DEPOSITS_START_BLOCK= +# INDEXER_OPTIMISM_L1_DEPOSITS_BATCH_SIZE= # ROOTSTOCK_REMASC_ADDRESS= # ROOTSTOCK_BRIDGE_ADDRESS= # ROOTSTOCK_LOCKED_BTC_CACHE_PERIOD= # ROOTSTOCK_LOCKING_CAP= +# INDEXER_DISABLE_ROOTSTOCK_DATA_FETCHER= +# INDEXER_ROOTSTOCK_DATA_FETCHER_INTERVAL= +# INDEXER_ROOTSTOCK_DATA_FETCHER_BATCH_SIZE= +# INDEXER_ROOTSTOCK_DATA_FETCHER_CONCURRENCY= +# INDEXER_ROOTSTOCK_DATA_FETCHER_DB_BATCH_SIZE= +# INDEXER_BEACON_RPC_URL=http://localhost:5052 +# INDEXER_DISABLE_BEACON_BLOB_FETCHER= +# INDEXER_BEACON_BLOB_FETCHER_SLOT_DURATION=12 +# INDEXER_BEACON_BLOB_FETCHER_REFERENCE_SLOT=8000000 +# INDEXER_BEACON_BLOB_FETCHER_REFERENCE_TIMESTAMP=1702824023 +# INDEXER_BEACON_BLOB_FETCHER_START_BLOCK=19200000 +# INDEXER_BEACON_BLOB_FETCHER_END_BLOCK=0 # TOKEN_ID_MIGRATION_FIRST_BLOCK= # TOKEN_ID_MIGRATION_CONCURRENCY= # TOKEN_ID_MIGRATION_BATCH_SIZE= @@ -174,42 +255,36 @@ COIN_BALANCE_HISTORY_DAYS=90 APPS_MENU=true EXTERNAL_APPS=[] # GAS_PRICE= +# GAS_PRICE_ORACLE_CACHE_PERIOD= +# GAS_PRICE_ORACLE_SIMPLE_TRANSACTION_GAS= +# GAS_PRICE_ORACLE_NUM_OF_BLOCKS= +# GAS_PRICE_ORACLE_SAFELOW_PERCENTILE= +# GAS_PRICE_ORACLE_AVERAGE_PERCENTILE= +# GAS_PRICE_ORACLE_FAST_PERCENTILE= +# GAS_PRICE_ORACLE_SAFELOW_TIME_COEFFICIENT= +# GAS_PRICE_ORACLE_AVERAGE_TIME_COEFFICIENT= +# GAS_PRICE_ORACLE_FAST_TIME_COEFFICIENT= # RESTRICTED_LIST= # RESTRICTED_LIST_KEY= SHOW_MAINTENANCE_ALERT=false MAINTENANCE_ALERT_MESSAGE= -SOURCIFY_INTEGRATION_ENABLED=false -SOURCIFY_SERVER_URL= -SOURCIFY_REPO_URL= CHAIN_ID= MAX_SIZE_UNLESS_HIDE_ARRAY=50 HIDE_BLOCK_MINER=false DISPLAY_TOKEN_ICONS=false -SHOW_TENDERLY_LINK=false -TENDERLY_CHAIN_PATH= RE_CAPTCHA_SECRET_KEY= RE_CAPTCHA_CLIENT_KEY= RE_CAPTCHA_V3_SECRET_KEY= RE_CAPTCHA_V3_CLIENT_KEY= RE_CAPTCHA_DISABLED=false JSON_RPC= -#API_RATE_LIMIT_DISABLED=true -API_RATE_LIMIT_TIME_INTERVAL=1s -API_RATE_LIMIT_BY_IP_TIME_INTERVAL=5m -API_RATE_LIMIT=50 -API_RATE_LIMIT_BY_KEY=50 -API_RATE_LIMIT_BY_WHITELISTED_IP=50 -API_RATE_LIMIT_WHITELISTED_IPS= -API_RATE_LIMIT_STATIC_API_KEY= -API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 -API_RATE_LIMIT_BY_IP=3000 -# API_RATE_LIMIT_HAMMER_REDIS_URL=redis://redis_db:6379/1 +# API_RATE_LIMIT_HAMMER_REDIS_URL=redis://redis-db:6379/1 # API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY=false API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS=18000 FETCH_REWARDS_WAY=trace_block MICROSERVICE_SC_VERIFIER_ENABLED=true -#MICROSERVICE_SC_VERIFIER_URL=http://smart-contract-verifier:8050/ -#MICROSERVICE_SC_VERIFIER_TYPE=sc_verifier +# MICROSERVICE_SC_VERIFIER_URL=http://smart-contract-verifier:8050/ +# MICROSERVICE_SC_VERIFIER_TYPE=sc_verifier MICROSERVICE_SC_VERIFIER_URL=https://eth-bytecode-db.services.blockscout.com/ MICROSERVICE_SC_VERIFIER_TYPE=eth_bytecode_db MICROSERVICE_ETH_BYTECODE_DB_INTERVAL_BETWEEN_LOOKUPS=10m @@ -218,6 +293,10 @@ MICROSERVICE_VISUALIZE_SOL2UML_ENABLED=true MICROSERVICE_VISUALIZE_SOL2UML_URL=http://visualizer:8050/ MICROSERVICE_SIG_PROVIDER_ENABLED=true MICROSERVICE_SIG_PROVIDER_URL=http://sig-provider:8050/ +# MICROSERVICE_BENS_URL= +# MICROSERVICE_BENS_ENABLED= +#MICROSERVICE_ACCOUNT_ABSTRACTION_ENABLED=true +#MICROSERVICE_ACCOUNT_ABSTRACTION_URL= DECODE_NOT_A_CONTRACT_CALLS=true # DATABASE_READ_ONLY_API_URL= # ACCOUNT_DATABASE_URL= @@ -230,24 +309,38 @@ DECODE_NOT_A_CONTRACT_CALLS=true # ACCOUNT_SENDGRID_API_KEY= # ACCOUNT_SENDGRID_SENDER= # ACCOUNT_SENDGRID_TEMPLATE= +# ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL= +# ACCOUNT_PRIVATE_TAGS_LIMIT=2000 +# ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15 ACCOUNT_CLOAK_KEY= ACCOUNT_ENABLED=false -ACCOUNT_REDIS_URL=redis://redis_db:6379 +ACCOUNT_REDIS_URL=redis://redis-db:6379 +EIP_1559_ELASTICITY_MULTIPLIER=2 # MIXPANEL_TOKEN= # MIXPANEL_URL= # AMPLITUDE_API_KEY= # AMPLITUDE_URL= -EIP_1559_ELASTICITY_MULTIPLIER=2 -# API_SENSITIVE_ENDPOINTS_KEY= -# ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL= -# INDEXER_TOKEN_INSTANCE_RETRY_BATCH_SIZE=10 -# INDEXER_TOKEN_INSTANCE_REALTIME_BATCH_SIZE=1 -# INDEXER_TOKEN_INSTANCE_SANITIZE_BATCH_SIZE=10 -# INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_BATCH_SIZE=10 -# TOKEN_INSTANCE_OWNER_MIGRATION_CONCURRENCY=5 -# TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE=50 # IPFS_GATEWAY_URL= -API_V2_ENABLED=true # ADDRESSES_TABS_COUNTERS_TTL=10m -# ACCOUNT_PRIVATE_TAGS_LIMIT=2000 -# ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15 +# DENORMALIZATION_MIGRATION_BATCH_SIZE= +# DENORMALIZATION_MIGRATION_CONCURRENCY= +# TOKEN_TRANSFER_TOKEN_TYPE_MIGRATION_BATCH_SIZE= +# TOKEN_TRANSFER_TOKEN_TYPE_MIGRATION_CONCURRENCY= +# SANITIZE_INCORRECT_NFT_BATCH_SIZE= +# SANITIZE_INCORRECT_NFT_CONCURRENCY= +SOURCIFY_INTEGRATION_ENABLED=false +SOURCIFY_SERVER_URL= +SOURCIFY_REPO_URL= +SHOW_TENDERLY_LINK=false +TENDERLY_CHAIN_PATH= +# SOLIDITYSCAN_CHAIN_ID= +# SOLIDITYSCAN_API_TOKEN= +# NOVES_FI_BASE_API_URL= +# NOVES_FI_CHAIN_NAME= +# NOVES_FI_API_TOKEN= +# BRIDGED_TOKENS_ENABLED= +# BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR= +# BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR= +# BRIDGED_TOKENS_POA_OMNI_BRIDGE_MEDIATOR= +# BRIDGED_TOKENS_AMB_BRIDGE_MEDIATORS +# BRIDGED_TOKENS_FOREIGN_JSON_RPC \ No newline at end of file diff --git a/docker-compose/docker-compose-no-build-erigon.yml b/docker-compose/erigon.yml similarity index 57% rename from docker-compose/docker-compose-no-build-erigon.yml rename to docker-compose/erigon.yml index 44e8f0441b78..231d2f92eb78 100644 --- a/docker-compose/docker-compose-no-build-erigon.yml +++ b/docker-compose/erigon.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend links: - db:database @@ -30,38 +33,40 @@ services: visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -70,5 +75,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/docker-compose-no-build-external-backend.yml b/docker-compose/external-backend.yml similarity index 50% rename from docker-compose/docker-compose-no-build-external-backend.yml rename to docker-compose/external-backend.yml index bd6acb24d8ee..82f03bf14dda 100644 --- a/docker-compose/docker-compose-no-build-external-backend.yml +++ b/docker-compose/external-backend.yml @@ -1,51 +1,57 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: + depends_on: + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -53,5 +59,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/docker-compose-no-build-no-db-container.yml b/docker-compose/external-db.yml similarity index 57% rename from docker-compose/docker-compose-no-build-no-db-container.yml rename to docker-compose/external-db.yml index a9a95c458b88..f1172c29f83f 100644 --- a/docker-compose/docker-compose-no-build-no-db-container.yml +++ b/docker-compose/external-db.yml @@ -1,54 +1,56 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db backend: depends_on: - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend environment: ETHEREUM_JSONRPC_VARIANT: 'geth' visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -57,5 +59,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/docker-compose-no-build-external-frontend.yml b/docker-compose/external-frontend.yml similarity index 61% rename from docker-compose/docker-compose-no-build-external-frontend.yml rename to docker-compose/external-frontend.yml index 71804ffeae73..6f3fbee2910d 100644 --- a/docker-compose/docker-compose-no-build-external-frontend.yml +++ b/docker-compose/external-frontend.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend links: - db:database @@ -34,31 +37,33 @@ services: visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -66,5 +71,5 @@ services: - backend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy \ No newline at end of file diff --git a/docker-compose/docker-compose-no-build-ganache.yml b/docker-compose/ganache.yml similarity index 59% rename from docker-compose/docker-compose-no-build-ganache.yml rename to docker-compose/ganache.yml index f86be323868b..136d5b25d2ad 100644 --- a/docker-compose/docker-compose-no-build-ganache.yml +++ b/docker-compose/ganache.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend links: - db:database @@ -34,38 +37,43 @@ services: visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend + environment: + NEXT_PUBLIC_NETWORK_ID: '1337' + NEXT_PUBLIC_NETWORK_RPC_URL: http://host.docker.internal:8545/ stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -74,5 +82,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/docker-compose-no-build-geth-clique-consensus.yml b/docker-compose/geth-clique-consensus.yml similarity index 58% rename from docker-compose/docker-compose-no-build-geth-clique-consensus.yml rename to docker-compose/geth-clique-consensus.yml index 4f605567e431..8d42b273d2d0 100644 --- a/docker-compose/docker-compose-no-build-geth-clique-consensus.yml +++ b/docker-compose/geth-clique-consensus.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend links: - db:database @@ -31,38 +34,40 @@ services: visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -71,5 +76,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/docker-compose-no-build-geth.yml b/docker-compose/geth.yml similarity index 57% rename from docker-compose/docker-compose-no-build-geth.yml rename to docker-compose/geth.yml index 94ccea912a2a..366630ce5296 100644 --- a/docker-compose/docker-compose-no-build-geth.yml +++ b/docker-compose/geth.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend links: - db:database @@ -30,38 +33,40 @@ services: visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -70,5 +75,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/docker-compose-no-build-hardhat-network.yml b/docker-compose/hardhat-network.yml similarity index 54% rename from docker-compose/docker-compose-no-build-hardhat-network.yml rename to docker-compose/hardhat-network.yml index defe8e7b35ff..1a014c72f713 100644 --- a/docker-compose/docker-compose-no-build-hardhat-network.yml +++ b/docker-compose/hardhat-network.yml @@ -1,27 +1,30 @@ version: '3.9' services: - redis_db: + redis-db: extends: - file: ./services/docker-compose-redis.yml - service: redis_db + file: ./services/redis.yml + service: redis-db db-init: extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db-init db: + depends_on: + db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-db.yml + file: ./services/db.yml service: db backend: depends_on: - db - - redis_db + - redis-db extends: - file: ./services/docker-compose-backend.yml + file: ./services/backend.yml service: backend links: - db:database @@ -29,41 +32,48 @@ services: ETHEREUM_JSONRPC_VARIANT: 'geth' ETHEREUM_JSONRPC_WS_URL: ws://host.docker.internal:8545/ INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER: 'true' + INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE: 'opcode' + CHAIN_ID: '31337' visualizer: extends: - file: ./services/docker-compose-visualizer.yml + file: ./services/visualizer.yml service: visualizer sig-provider: extends: - file: ./services/docker-compose-sig-provider.yml + file: ./services/sig-provider.yml service: sig-provider frontend: depends_on: - backend extends: - file: ./services/docker-compose-frontend.yml + file: ./services/frontend.yml service: frontend + environment: + NEXT_PUBLIC_NETWORK_ID: '31337' + NEXT_PUBLIC_NETWORK_RPC_URL: http://host.docker.internal:8545/ stats-db-init: extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db-init stats-db: depends_on: - - backend + stats-db-init: + condition: service_completed_successfully extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats-db stats: depends_on: - stats-db + - backend extends: - file: ./services/docker-compose-stats.yml + file: ./services/stats.yml service: stats proxy: @@ -72,5 +82,5 @@ services: - frontend - stats extends: - file: ./services/docker-compose-nginx.yml + file: ./services/nginx.yml service: proxy diff --git a/docker-compose/microservices.yml b/docker-compose/microservices.yml new file mode 100644 index 000000000000..38735e73437a --- /dev/null +++ b/docker-compose/microservices.yml @@ -0,0 +1,53 @@ +version: '3.9' + +services: + visualizer: + extends: + file: ./services/visualizer.yml + service: visualizer + + sig-provider: + extends: + file: ./services/sig-provider.yml + service: sig-provider + ports: + - 8083:8050 + + + sc-verifier: + extends: + file: ./services/smart-contract-verifier.yml + service: smart-contract-verifier + ports: + - 8082:8050 + + stats-db-init: + extends: + file: ./services/stats.yml + service: stats-db-init + + stats-db: + depends_on: + stats-db-init: + condition: service_completed_successfully + extends: + file: ./services/stats.yml + service: stats-db + + stats: + depends_on: + - stats-db + - backend + extends: + file: ./services/stats.yml + service: stats + + proxy: + depends_on: + - visualizer + - stats + extends: + file: ./services/nginx.yml + service: proxy + volumes: + - "./proxy/microservices.conf.template:/etc/nginx/templates/default.conf.template" diff --git a/docker-compose/proxy/microservices.conf.template b/docker-compose/proxy/microservices.conf.template new file mode 100644 index 000000000000..708812f57113 --- /dev/null +++ b/docker-compose/proxy/microservices.conf.template @@ -0,0 +1,65 @@ +map $http_upgrade $connection_upgrade { + + default upgrade; + '' close; +} + +server { + listen 8080; + server_name localhost; + proxy_http_version 1.1; + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Methods; + add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'PUT, GET, POST, OPTIONS, DELETE, PATCH' always; + + location / { + proxy_pass http://stats:8050/; + proxy_http_version 1.1; + proxy_set_header Host "$host"; + proxy_set_header X-Real-IP "$remote_addr"; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header Upgrade "$http_upgrade"; + proxy_set_header Connection $connection_upgrade; + proxy_cache_bypass $http_upgrade; + } +} +server { + listen 8081; + server_name localhost; + proxy_http_version 1.1; + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Methods; + add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'PUT, GET, POST, OPTIONS, DELETE, PATCH' always; + add_header 'Access-Control-Allow-Headers' 'DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token' always; + + location / { + proxy_pass http://visualizer:8050/; + proxy_http_version 1.1; + proxy_buffering off; + proxy_set_header Host "$host"; + proxy_set_header X-Real-IP "$remote_addr"; + proxy_connect_timeout 30m; + proxy_read_timeout 30m; + proxy_send_timeout 30m; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header Upgrade "$http_upgrade"; + proxy_set_header Connection $connection_upgrade; + proxy_cache_bypass $http_upgrade; + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'PUT, GET, POST, OPTIONS, DELETE, PATCH' always; + add_header 'Access-Control-Allow-Headers' 'DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + } +} \ No newline at end of file diff --git a/docker-compose/services/docker-compose-backend.yml b/docker-compose/services/backend.yml similarity index 79% rename from docker-compose/services/docker-compose-backend.yml rename to docker-compose/services/backend.yml index d5f934fb653f..cf8a0871d366 100644 --- a/docker-compose/services/docker-compose-backend.yml +++ b/docker-compose/services/backend.yml @@ -2,7 +2,7 @@ version: '3.9' services: backend: - image: blockscout/blockscout:${DOCKER_TAG:-latest} + image: blockscout/${DOCKER_REPO:-blockscout}:${DOCKER_TAG:-latest} pull_policy: always restart: always stop_grace_period: 5m diff --git a/docker-compose/services/docker-compose-db.yml b/docker-compose/services/db.yml similarity index 76% rename from docker-compose/services/docker-compose-db.yml rename to docker-compose/services/db.yml index 131d90bb931c..430409bbecfe 100644 --- a/docker-compose/services/docker-compose-db.yml +++ b/docker-compose/services/db.yml @@ -2,7 +2,7 @@ version: '3.9' services: db-init: - image: postgres:14 + image: postgres:15 volumes: - ./blockscout-db-data:/var/lib/postgresql/data entrypoint: @@ -12,20 +12,19 @@ services: chown -R 2000:2000 /var/lib/postgresql/data db: - depends_on: - db-init: - condition: service_completed_successfully - image: postgres:14 + image: postgres:15 user: 2000:2000 + shm_size: 256m restart: always container_name: 'db' - command: postgres -c 'max_connections=200' + command: postgres -c 'max_connections=200' -c 'client_connection_check_interval=60000' environment: POSTGRES_DB: 'blockscout' POSTGRES_USER: 'blockscout' POSTGRES_PASSWORD: 'ceWb1MeLBEeOIfk65gU8EjF8' ports: - - 7432:5432 + - target: 5432 + published: 7432 volumes: - ./blockscout-db-data:/var/lib/postgresql/data healthcheck: diff --git a/docker-compose/services/docker-compose-frontend.yml b/docker-compose/services/frontend.yml similarity index 100% rename from docker-compose/services/docker-compose-frontend.yml rename to docker-compose/services/frontend.yml diff --git a/docker-compose/services/docker-compose-nginx.yml b/docker-compose/services/nginx.yml similarity index 72% rename from docker-compose/services/docker-compose-nginx.yml rename to docker-compose/services/nginx.yml index 27c0a0a6a662..bc225c9082b6 100644 --- a/docker-compose/services/docker-compose-nginx.yml +++ b/docker-compose/services/nginx.yml @@ -12,6 +12,9 @@ services: BACK_PROXY_PASS: ${BACK_PROXY_PASS:-http://backend:4000} FRONT_PROXY_PASS: ${FRONT_PROXY_PASS:-http://frontend:3000} ports: - - 80:80 - - 8080:8080 - - 8081:8081 + - target: 80 + published: 80 + - target: 8080 + published: 8080 + - target: 8081 + published: 8081 diff --git a/docker-compose/services/docker-compose-redis.yml b/docker-compose/services/redis.yml similarity index 74% rename from docker-compose/services/docker-compose-redis.yml rename to docker-compose/services/redis.yml index 9760137f5bb5..93f616686de6 100644 --- a/docker-compose/services/docker-compose-redis.yml +++ b/docker-compose/services/redis.yml @@ -1,9 +1,9 @@ version: '3.9' services: - redis_db: + redis-db: image: 'redis:alpine' - container_name: redis_db + container_name: redis-db command: redis-server volumes: - ./redis-data:/data diff --git a/docker-compose/services/docker-compose-sig-provider.yml b/docker-compose/services/sig-provider.yml similarity index 100% rename from docker-compose/services/docker-compose-sig-provider.yml rename to docker-compose/services/sig-provider.yml diff --git a/docker-compose/services/docker-compose-smart-contract-verifier.yml b/docker-compose/services/smart-contract-verifier.yml similarity index 100% rename from docker-compose/services/docker-compose-smart-contract-verifier.yml rename to docker-compose/services/smart-contract-verifier.yml diff --git a/docker-compose/services/docker-compose-stats.yml b/docker-compose/services/stats.yml similarity index 77% rename from docker-compose/services/docker-compose-stats.yml rename to docker-compose/services/stats.yml index 5077a5d893c2..83c9ea02a22b 100644 --- a/docker-compose/services/docker-compose-stats.yml +++ b/docker-compose/services/stats.yml @@ -2,7 +2,7 @@ version: '3.9' services: stats-db-init: - image: postgres:14 + image: postgres:15 volumes: - ./stats-db-data:/var/lib/postgresql/data entrypoint: @@ -12,20 +12,19 @@ services: chown -R 2000:2000 /var/lib/postgresql/data stats-db: - depends_on: - stats-db-init: - condition: service_completed_successfully - image: postgres:14 + image: postgres:15 user: 2000:2000 + shm_size: 256m restart: always - container_name: 'stats-postgres' + container_name: 'stats-db' command: postgres -c 'max_connections=200' environment: POSTGRES_DB: 'stats' POSTGRES_USER: 'stats' POSTGRES_PASSWORD: 'n0uejXPl61ci6ldCuE2gQU5Y' ports: - - 7433:5432 + - target: 5432 + published: 7433 volumes: - ./stats-db-data:/var/lib/postgresql/data healthcheck: @@ -41,14 +40,12 @@ services: platform: linux/amd64 restart: always container_name: 'stats' - depends_on: - - "stats-db" extra_hosts: - 'host.docker.internal:host-gateway' env_file: - ../envs/common-stats.env environment: - STATS__DB_URL=postgres://stats:n0uejXPl61ci6ldCuE2gQU5Y@stats-db:5432/stats - - STATS__BLOCKSCOUT_DB_URL=postgresql://blockscout:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout + - STATS__BLOCKSCOUT_DB_URL=${STATS__BLOCKSCOUT_DB_URL:-postgresql://blockscout:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout} - STATS__CREATE_DATABASE=true - STATS__RUN_MIGRATIONS=true diff --git a/docker-compose/services/docker-compose-visualizer.yml b/docker-compose/services/visualizer.yml similarity index 100% rename from docker-compose/services/docker-compose-visualizer.yml rename to docker-compose/services/visualizer.yml diff --git a/docker/Dockerfile b/docker/Dockerfile index bc8bb581b4b8..1990e9ee0ed1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,6 +23,8 @@ ARG AMPLITUDE_API_KEY ARG AMPLITUDE_URL ARG CHAIN_TYPE ENV CHAIN_TYPE=${CHAIN_TYPE} +ARG BRIDGED_TOKENS_ENABLED +ENV BRIDGED_TOKENS_ENABLED=${BRIDGED_TOKENS_ENABLED} # Cache elixir deps ADD mix.exs mix.lock ./ @@ -70,6 +72,8 @@ ARG RELEASE_VERSION ENV RELEASE_VERSION=${RELEASE_VERSION} ARG CHAIN_TYPE ENV CHAIN_TYPE=${CHAIN_TYPE} +ARG BRIDGED_TOKENS_ENABLED +ENV BRIDGED_TOKENS_ENABLED=${BRIDGED_TOKENS_ENABLED} ARG BLOCKSCOUT_VERSION ENV BLOCKSCOUT_VERSION=${BLOCKSCOUT_VERSION} diff --git a/docker/Makefile b/docker/Makefile index c6e874b4682c..02496904fd38 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -7,28 +7,28 @@ FRONTEND_CONTAINER_NAME := frontend VISUALIZER_CONTAINER_NAME := visualizer SIG_PROVIDER_CONTAINER_NAME := sig-provider STATS_CONTAINER_NAME := stats -STATS_DB_CONTAINER_NAME := stats-postgres +STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '5.3.1' +RELEASE_VERSION ?= '6.3.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) start: @echo "==> Starting blockscout db" - @docker-compose -f ../docker-compose/services/docker-compose-db.yml up -d + @docker-compose -f ../docker-compose/services/db.yml up -d @echo "==> Starting blockscout backend" - @docker-compose -f ../docker-compose/services/docker-compose-backend.yml up -d + @docker-compose -f ../docker-compose/services/backend.yml up -d @echo "==> Starting stats microservice" - @docker-compose -f ../docker-compose/services/docker-compose-stats.yml up -d + @docker-compose -f ../docker-compose/services/stats.yml up -d @echo "==> Starting visualizer microservice" - @docker-compose -f ../docker-compose/services/docker-compose-visualizer.yml up -d + @docker-compose -f ../docker-compose/services/visualizer.yml up -d @echo "==> Starting sig-provider microservice" - @docker-compose -f ../docker-compose/services/docker-compose-sig-provider.yml up -d + @docker-compose -f ../docker-compose/services/sig-provider.yml up -d @echo "==> Starting blockscout frontend" - @docker-compose -f ../docker-compose/services/docker-compose-frontend.yml up -d + @docker-compose -f ../docker-compose/services/frontend.yml up -d @echo "==> Starting Nginx proxy" - @docker-compose -f ../docker-compose/services/docker-compose-nginx.yml up -d + @docker-compose -f ../docker-compose/services/nginx.yml up -d BS_BACKEND_STARTED := $(shell docker ps --no-trunc --filter name=^/${BACKEND_CONTAINER_NAME}$ | grep ${BACKEND_CONTAINER_NAME}) BS_FRONTEND_STARTED := $(shell docker ps --no-trunc --filter name=^/${FRONTEND_CONTAINER_NAME}$ | grep ${FRONTEND_CONTAINER_NAME}) diff --git a/docker/README.md b/docker/README.md index 03df272c4a12..a1078f86a28f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,5 +1,3 @@ # BlockScout Docker Integration -This integration is not production ready, and should be used for local BlockScout deployment only. - -For usage instructions and ENV variables, see the [docker integration documentation](https://docs.blockscout.com/for-developers/information-and-settings/docker-integration-local-use-only). \ No newline at end of file +For usage instructions and ENV variables, see the [docker integration documentation](https://docs.blockscout.com/for-developers/deployment/docker-compose-deployment). \ No newline at end of file diff --git a/mix.exs b/mix.exs index 12ddf19b66ee..70e3fced9a9a 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "5.3.1", + version: "6.3.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), @@ -52,8 +52,8 @@ defmodule BlockScout.Mixfile do defp dialyzer() do [ - plt_add_deps: :transitive, - plt_add_apps: ~w(ex_unit mix)a, + plt_add_deps: :app_tree, + plt_add_apps: ~w(ex_unit mix wallaby)a, ignore_warnings: ".dialyzer-ignore", plt_core_path: "priv/plts", plt_file: {:no_warn, "priv/plts/dialyzer.plt"} @@ -96,7 +96,7 @@ defmodule BlockScout.Mixfile do {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.3", override: true}, {:tesla, "~> 1.8.0"}, # Documentation - {:ex_doc, "~> 0.30.1", only: :dev, runtime: false}, + {:ex_doc, "~> 0.31.0", only: :dev, runtime: false}, {:number, "~> 1.0.3"} ] end diff --git a/mix.lock b/mix.lock index 9fcd975445b3..1cf70d6fb22f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,21 +1,21 @@ %{ - "absinthe": {:hex, :absinthe, "1.7.5", "a15054f05738e766f7cc7fd352887dfd5e61cec371fb4741cca37c3359ff74ac", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "22a9a38adca26294ad0ee91226168f5d215b401efd770b8a1b8fd9c9b21ec316"}, + "absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"}, "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.2", "e607b438db900049b9b3760f8ecd0591017a46122fffed7057bf6989020992b5", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "d36918925c380dc7d2ed7d039c9a3b4182ec36723f7417a68745ade5aab22f8d"}, "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "c435d43f316769e1beee1dbe500b623124c96785", [tag: "1.5.3"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "bamboo": {:hex, :bamboo, "2.3.0", "d2392a2cabe91edf488553d3c70638b532e8db7b76b84b0a39e3dfe492ffd6fc", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dd0037e68e108fd04d0e8773921512c940e35d981e097b5793543e3b2f9cd3f6"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, - "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, + "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, "benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"}, - "briefly": {:git, "https://github.com/CargoSense/briefly.git", "51dfe7fbe0f897ea2a921d9af120762392aca6a1", []}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "briefly": {:git, "https://github.com/CargoSense/briefly.git", "4836ba322ffb504a102a15cc6e35d928ef97120e", []}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bureaucrat": {:hex, :bureaucrat, "0.2.9", "d98e4d2b9bdbf22e4a45c2113ce8b38b5b63278506c6ff918e3b943a4355d85b", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "111c8dd84382a62e1026ae011d592ceee918553e5203fe8448d9ba6ccbdfff7d"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, - "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, + "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, - "cldr_utils": {:hex, :cldr_utils, "2.24.1", "5ff8c8c55f96666228827bcf85a23d632022def200566346545d01d15e4c30dc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "1820300531b5b849d0bc468e5a87cd64f8f2c5191916f548cbe69b2efc203780"}, + "cldr_utils": {:hex, :cldr_utils, "2.24.2", "364fa30be55d328e704629568d431eb74cd2f085752b27f8025520b566352859", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3362b838836a9f0fa309de09a7127e36e67310e797d556db92f71b548832c7cf"}, "cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"}, "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, @@ -27,28 +27,28 @@ "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "1.0.11", "49bbfc7dd8a1990423c51000b869b1fecaab9e3ccd6b29eab51616ae8ad0a2f5", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba0b0ec532ec68e9d033d03553561d693129bd7cbd5c649dc7903f07ffba08fe"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "digital_token": {:hex, :digital_token, "0.6.0", "13e6de581f0b1f6c686f7c7d12ab11a84a7b22fa79adeb4b50eec1a2d278d258", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2455d626e7c61a128b02a4a8caddb092548c3eb613ac6f6a85e4cbb6caddc4d1"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"}, - "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, - "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, + "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_abi": {:hex, :ex_abi, "0.6.3", "e16f232346badc9f03a459cea7113ad4cff28dd2faf36f5635eaa01d5e903d7c", [:mix], [{:ex_keccak, "~> 0.7.3", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "bd1bcbe1d4fac14b90aa132fa54690b36d26e11e3777b755623bf32e66caef85"}, - "ex_cldr": {:hex, :ex_cldr, "2.37.4", "3e2c04d9c691a75a8b7e808dfcbacedb9cdf3e73f819d1b4174f8f065e5f29c1", [:mix], [{:cldr_utils, "~> 2.21", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "2bcb5de0095324ba645b4e156bf346add156d8b928f0ffd985c61664d981ad3d"}, - "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.0", "aadd34e91cfac7ef6b03fe8f47f8c6fa8c5daf3f89b5d9fee64ec545ded839cf", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "0521316396c66877a2d636219767560bb2397c583341fcb154ecf9f3000e6ff8"}, - "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.10.1", "67795eb28f8534a36cbdfeeaea6d3ee587eeac7eafff71968dd046c215d4ec42", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "da826148d95c7a1889fd847ae5704c141747bad43a5a44431ae97bced57b0f93"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.2", "5e0e3031d3f54b51fe7078a7a94592987b70b06d631bdc88813b222dc5a8b1bd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "91257684a9c4d6abdf738f0cc5671837de876e69552e8bd4bc5fa1bfd5817713"}, - "ex_cldr_units": {:hex, :ex_cldr_units, "3.16.3", "41344ed9f6d5c69baeb4d13dadc80310ca511626f738af2e69ae53dd40fb28a5", [:mix], [{:cldr_utils, "~> 2.24", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "889dd56084821723ce82c4639d62cfc3513a9ca1ffb9063bcc83e7b3de5bc35b"}, - "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, - "ex_json_schema": {:hex, :ex_json_schema, "0.10.1", "e03b746b6675a750c0bb1a5cc919f61353f7ab8450977e11ceede20e6180c560", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "66a64e60dadad89914d92f89c7e7906c57de75a8b79ac2480d0d53e1b8096fb0"}, + "ex_abi": {:hex, :ex_abi, "0.7.1", "8136d8363653418442848009ed5d31312161781326d867e77467608b2d5ff2c9", [:mix], [{:ex_keccak, "~> 0.7.3", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41ad2de3f62b6e79f7e865c7ab03f58e0d44435325ff15e345a986e0387a2a04"}, + "ex_cldr": {:hex, :ex_cldr, "2.37.5", "9da6d97334035b961d2c2de167dc6af8cd3e09859301a5b8f49f90bd8b034593", [:mix], [{:cldr_utils, "~> 2.21", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "74ad5ddff791112ce4156382e171a5f5d3766af9d5c4675e0571f081fe136479"}, + "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.1", "e92ba17c41e7405b7784e0e65f406b5f17cfe313e0e70de9befd653e12854822", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "31df8bd37688340f8819bdd770eb17d659652078d34db632b85d4a32864d6a25"}, + "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.10.2", "c8dbb3324ca35cea3679a96f4c774cdf4bdd425786a44c4f52aacb57a0cee446", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "0cc2124eccffa5438045c2504dd3365490b64065131f58ecc27f344db1edb4b8"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.4", "5562148dfc631b04712983975093d2aac29df30b3bf2f7257e0c94b85b72e91b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6fd5a82f0785418fa8b698c0be2b1845dff92b77f1b3172c763d37868fb503d2"}, + "ex_cldr_units": {:hex, :ex_cldr_units, "3.16.4", "fee054e9ebed40ef05cbb405cb0c7e7c9fda201f8f03ec0d1e54e879af413246", [:mix], [{:cldr_utils, "~> 2.24", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "7c15c6357dd555a5bc6c72fdeb243e4706a04065753dbd2f40150f062ca996c7"}, + "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.10.2", "7c4b8c1481fdeb1741e2ce66223976edfb9bccebc8014f6aec35d4efe964fb71", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "37f43be60f8407659d4d0155a7e45e7f406dab1f827051d3d35858a709baf6a6"}, "ex_keccak": {:hex, :ex_keccak, "0.7.3", "33298f97159f6b0acd28f6e96ce5ea975a0f4a19f85fe615b4f4579b88b24d06", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4c5e6d9d5f77b64ab48769a0166a9814180d40ced68ed74ce60a5174ab55b3fc"}, "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, @@ -56,28 +56,29 @@ "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, - "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, - "exvcr": {:hex, :exvcr, "0.14.4", "1aa5fe7d3f10b117251c158f8d28b39f7fc73d0a7628b2d0b75bf8cfb1111576", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4e600568c02ed29d46bc2e2c74927d172ba06658aa8b14705c0207363c44cc94"}, + "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, + "exvcr": {:hex, :exvcr, "0.15.1", "772db4d065f5136c6a984c302799a79e4ade3e52701c95425fa2229dd6426886", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "de4fc18b1d672d9b72bc7468735e19779aa50ea963a1f859ef82cd9e294b13e3"}, "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "floki": {:hex, :floki, "0.35.1", "b21cf592ed38c1207c5ea52120a2e81d6ecba11337a633a3f29ec17a64033178", [:mix], [], "hexpm", "f126e3eb814f131c21befeeeb773d2c4e2331ce05214c1a9844a3edde5c69003"}, + "floki": {:hex, :floki, "0.36.0", "544d5dd8a3107f660633226b5805e47c2ac1fabd782fae86e3b22b02849b20f9", [:mix], [], "hexpm", "ab1ca4b1efb0db00df9a8e726524e2c85be88cf65ac092669186e1674d34d74c"}, "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, - "gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"}, + "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"}, + "hammer": {:hex, :hammer, "6.2.1", "5ae9c33e3dceaeb42de0db46bf505bd9c35f259c8defb03390cd7556fea67ee2", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b9476d0c13883d2dc0cc72e786bac6ac28911fba7cc2e04b70ce6a6d9c4b2bdc"}, "hammer_backend_redis": {:hex, :hammer_backend_redis, "6.1.2", "eb296bb4924928e24135308b2afc189201fd09411c870c6bbadea444a49b2f2c", [:mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "217ea066278910543a5e9b577d5bf2425419446b94fe76bdd9f255f39feec9fa"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"}, + "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.13", "df07b14970e9ac1f57362985d76e6f24e3e1ab05c248055b7d223976881977c2", [:mix], [], "hexpm", "71a453a7e6e899ae4549fb147b1c6621f4233f8f48f58ca10a64ec67b6c50018"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "logger_json": {:hex, :logger_json, "5.1.4", "9e30a4f2e31a8b9e402bdc20bd37cf9b67d3a31f19d0b33082a19a06b4c50f6d", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "3f20eea58e406a33d3eb7814c7dff5accb503bab2ee8601e84da02976fa3934c"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, "math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, @@ -90,8 +91,8 @@ "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, - "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "number": {:hex, :number, "1.0.4", "3e6e6032a3c1d4c3760e77a42c580a57a15545dd993af380809da30fe51a032c", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "16f7516584ef2be812af4f33f2eaf3f9b9f6ed8892f45853eb93113f83721e42"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "oauth2": {:hex, :oauth2, "2.0.1", "70729503e05378697b958919bb2d65b002ba6b28c8112328063648a9348aaa3f", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "c64e20d4d105bcdbcbe03170fb530d0eddc3a3e6b135a87528a22c8aecf74c52"}, @@ -99,17 +100,17 @@ "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.5.14", "2d5db884be496eefa5157505ec0134e66187cb416c072272420c5509d67bf808", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "207f1aa5520320cbb7940d7ff2dde2342162cf513875848f88249ea0ba02fef7"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"}, "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, + "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, - "prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"}, + "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, + "prometheus": {:hex, :prometheus, "4.11.0", "b95f8de8530f541bd95951e18e355a840003672e5eda4788c5fa6183406ba29a", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "719862351aabf4df7079b05dc085d2bbcbe3ac0ac3009e956671b1d5ab88247d"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"}, @@ -120,7 +121,7 @@ "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"}, - "redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"}, + "redix": {:hex, :redix, "1.4.1", "8303e13bad38ca80c15bdf79ea9cbd6eb879554c9cbb815b35df1602d7b1549d", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "676b5ce37d7b1d46931d506e3208786bd8334a1625ecb591d87d790b23ffbd1f"}, "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.6.3", "f838d94bc35e1844973ee7266127b156fdc962e9e8b7ff666c8fb4fed7964d23", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e18ecca3669a7454b3a2be75ae6c3ef01d550bc9a8cf5fbddcfff843b881d7c6"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, @@ -134,10 +135,12 @@ "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"}, + "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, - "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, + "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.1.0", "0632d5844049fa2f26823f15e1120aa32f27df6f27ce515a4b04641736594bf4", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "8d3b30fa27c95c9e82c30c4afb016251405706d2e9627e603c3c9787fd1314fc"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "varint": {:hex, :varint, "1.4.0", "b7405c8a99db7b95d4341fa9cb15e7c3af6c8dda43e21bbe1c4a9cdff50b6502", [:mix], [], "hexpm", "0fd461901b7120c03467530dff3c58fa3475328fd75ba72c7d3cbf13bce6b0d2"}, "wallaby": {:hex, :wallaby, "0.30.6", "7dc4c1213f3b52c4152581d126632bc7e06892336d3a0f582853efeeabd45a71", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "50950c1d968549b54c20e16175c68c7fc0824138e2bb93feb11ef6add8eb23d4"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websocket_client": {:git, "https://github.com/blockscout/websocket_client.git", "0b4ecc5b1fb8a0bd1c8352728da787c20add53aa", [branch: "master"]}, diff --git a/rel/config.exs b/rel/config.exs index a0584a8168b3..6b33ba9bf76f 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "5.3.1-beta" + set version: "6.3.0-beta" set applications: [ :runtime_tools, block_scout_web: :permanent,